block by emeeks 703bb8f302443dbbd7e7

d3.sketchy bug

Full Screen

A bug with d3.sketchy.

index.html

<html>
	<head>
		<style type="text/css">
			body,html{
				margin:0;
				padding:0;
			}

			#svg{
				width:1000px;
				height:1000px;
				margin:30px 0 0 30px;
			}

			rect{
				fill:none;
				stroke:#000;
			}

			path{
				fill:none;
				stroke:#000;
				stroke-width:0.3;
			}
		</style>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js" charset="utf-8" type="text/javascript"></script>
		<script src="d3.sketchy.js"></script>
	</head>
	<body>
		<svg id="svg" width="1000" height="1000"></svg>
		<script type="text/javascript">
			var svg = d3.select("#svg");

			var sketchy = new d3sketchy();

			for(var i = 0; i<19; i++){

				sketchy.rectFill({
					x:25,
					y:110 * i,
					width:200,
					height:100,
					svg: svg,
					density:1,
					angle:i*10,
					sketch: 0.5
				});
			}

		</script>
	</body>
</html>

d3.sketchy.js

/*global d3:false */
/*jshint unused:false*/

/**
 * Initiate the sketchy library
 * @constructor
 */
var d3sketchy = function(){

	/**
	* Default attributes for generating the shapes, doing this we don't need to check if all parameters are provided
	* And if someone wants to build a lot of shapes with the same properties she just needs to use "setDefaults" to change them
	* @type {object}
	* @defaultvalue
	*/
	var defaults = {
		x:0,
		y:0,
		width:20,
		height:20,
		sketch:1,
		density:1,
		radius:10,
		angle:45,
		count:2,
		shape:"circle",
		clip:"",
		margin:2
	};

	/**
	* Changing the default attributes
	* @param {object} opts - object with default attributes see "var defaults"
	* @return {object} defaults - the full default object
	*/
	function setDefaults(opts){
		defaults = extend(defaults, opts);
		return defaults;
	}

	/**
	* merging two objects, source will replace duplicates in destination
	* @param {object} destination
	* @param {object} source
	*/
	function extend(destination, source) {
		var returnObj = {}, attrname;
		for (attrname in destination) { returnObj[attrname] = destination[attrname]; }
		for (attrname in source) { returnObj[attrname] = source[attrname]; }
		return returnObj;
	}

	/**
	* Generate random number between min and max
	* @param {float|int} min
	* @param {float|int} max
	* @return {float}
	*/
	function rand(min, max){
		return Math.random()* (max-min) + min;
	}

	/**
	* Create sketchy
	* @constructor
	*/
	function sketchy(){
	}

	/**
	* drawing a sketchy line
	* this is kind of the heart of the whole tool.
	* so if you want to make changes to the appearance of the lines, tweak the following lines
	* @param {object} opts
	* @param {d3.selection} opts.svg
	* @param {float|int} opts.x1 - x point 1
	* @param {float|int} opts.y1 - y point 1
	* @param {float|int} opts.x2 - x point 2
	* @param {float|int} opts.y2 - y point 2
	* @param {object} opts.sketch
	* @param {object} opts.sketch.x - sketchiness on the x-axis
	* @param {object} opts.sketch.y - sketchiness on the y-axis
	*/
	sketchy.drawLine = function(opts){
		//Each line is drawn twice the increase sketchiness
		for(var i = 1; i<3; i++){

			var or2 = rand(0.2, 0.8);

			var cx2 = opts.x1+ (opts.x2-opts.x1)*or2+rand(-1,1);
			var cy2 = opts.y1+ (opts.y2-opts.y1)*or2+rand(-1,1);

			var or1 = or2 + rand(-0.3, -0.2);

			var cx1 = opts.x1+ (opts.x2-opts.x1)*or1+rand(-1,1);
			var cy1 = opts.y1+ (opts.y2-opts.y1)*or1+rand(-1,1);

			opts.svg.append("path")
				.attr("d", "M"+
								(opts.x1 + rand(-1,0)*opts.sketch.x/i)+" "+
								(opts.y1 + rand(-1,1)*opts.sketch.y/i)+" Q"+
								cx1+" "+cy1+" "+
								cx2+" "+cy2+" T"+
								(opts.x2 + rand(0,1)*opts.sketch.x/i)+" "+
								(opts.y2 + rand(-1,1)*opts.sketch.y/i));

		}
	};

	/**
	* drawing a circle shape
	* no outline just the fill
	* @param {object} opts - object containing the attributes
	* @param {float|int} opts.x - x position
	* @param {float|int} opts.y - y position
	* @param {float|int} opts.r - radius
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @param {string} opts.shape - this is a development relic, default is "circle", alternatives "cut" and "star"
	* @return {object} svg - d3.selection of a group object, containing the circle
	*/
	sketchy.circleFill = function(opts){
		//merging default attributes with user attributes
		var merged_opts = extend(defaults, opts);

		//create a container, this is used to translate and rotate the circle, this container will be returned at the end of this function
		var svg = merged_opts.svg.append("g").attr("transform", "translate("+merged_opts.x+" "+merged_opts.y+") rotate("+merged_opts.angle+")");

		//Looping through the lines
		var y_dist = 0;
		while(y_dist > -2*opts.r){
			var x;
			//During the development i accidentaly generated those shapes and kept them :)
			if(merged_opts.shape==="cut"){
				x = Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
			}else if(merged_opts.shape==="star"){
				x = merged_opts.r - Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
			}else{
				x = Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
			}

			//Draw the sketchy lines
			sketchy.drawLine({
				svg:svg,
				x1:-x,
				y1:y_dist+merged_opts.r,
				x2:x,
				y2:y_dist+merged_opts.r,
				sketch:{
					x:merged_opts.density*merged_opts.sketch,
					y:merged_opts.density*merged_opts.sketch
				}
			});

			y_dist -= merged_opts.density;
		}

		return svg;
	};

	/**
	* draws a rectangle
	* no outline just the fill
	* @param {object} opts - object containing the attributes
	* @param {float|int} opts.x - x position
	* @param {float|int} opts.y - y position
	* @param {float|int} opts.width - width
	* @param {float|int} opts.height - height
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {object} svg - d3.selection of a group object, containing the rectangle
	*/
	sketchy.rectFill = function(opts){
		var svg = opts.svg.append("g").attr("transform", "translate("+opts.x+" "+opts.y+")");
		opts.svg = svg;
		return sketchy.drawPattern(opts);
	};

	/**
	* draws a background pattern in the shape of a square according to x,y,with,height
	* @param {object} opts - object containing the attributes
	* @param {float|int} opts.x - x position
	* @param {float|int} opts.y - y position
	* @param {float|int} opts.width - width
	* @param {float|int} opts.height - height
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {object} svg - d3.selection of a group object, containing the background
	*/
	sketchy.drawPattern = function(opts){
		var svg = opts.svg;

		//angle for strokes
		var angle = opts.angle;
		while(angle > 360){angle -= 360;}
		if(angle > 180){angle -= 180;}
		var radian = (Math.PI/180)*(90-angle);
		var vector = {
			y:1,
			x:-1/Math.tan(radian)
		};

		//distance between strokes
		var dist = opts.density;

		var vy, tx, ty, vx, y1, x1, y_dist, x_dist;

		var x = opts.x, y = opts.y;
		if(Math.abs(angle) === 90){
			while(y < opts.y+opts.height){
				sketchy.drawLine({
					svg:svg,
					x1:x,
					y1:y,
					x2:x+opts.width,
					y2:y,
					sketch:{
						x:dist*opts.sketch,
						y:dist*opts.sketch
					}
				});
				y += dist;
			}
		}else if((Math.abs(angle) === 180)||(angle === 0)){
			while(x < opts.x+opts.width){
				sketchy.drawLine({
					svg:svg,
					x1:x,
					y1:y,
					x2:x,
					y2:y+opts.height,
					sketch:{
						x:dist*opts.sketch,
						y:dist*opts.sketch
					}
				});
				x += dist;
			}
		}else if(angle < 90){
			y_dist = Math.abs(dist / Math.sin(Math.PI/180*angle));
			x_dist = Math.abs(dist / Math.sin(Math.PI/180*(90-angle)));

			y += y_dist;
			y1 = opts.y;
			x1 = opts.x;
			while(y1 < opts.y+opts.height){
				vx = opts.width / vector.x;
				x1 = opts.width + x;
				y1 = y + vector.y * vx;
				ty = y;
				tx = x;

				if(y1<opts.y){
					vy = (y-opts.y)/vector.y;
					x1 = x + Math.abs(vector.x) * vy;
					y1 = opts.y;
				}else if(y > (opts.y+opts.height)){
					ty = opts.y+opts.height;
					vy = (ty-y1)/vector.y;
					tx = x + opts.width - vy*Math.abs(vector.x);
				}

				sketchy.drawLine({
					svg:svg,
					x1:tx,
					y1:ty,
					x2:x1,
					y2:y1,
					sketch:{
						x:x_dist*opts.sketch,
						y:y_dist*opts.sketch
					}
				});

				y += y_dist;
			}

		}else{
			y_dist = Math.abs(dist / Math.sin(Math.PI/180*angle));
			x_dist = Math.abs(dist / Math.sin(Math.PI/180*(180-angle)));

			y = opts.y+opts.height;
			y -= y_dist;
			y1 = opts.y+opts.height;
			x1 = opts.x;
			while(y1 > opts.y){
				vx = opts.width / vector.x;
				x1 = opts.width + x;
				y1 = y + vector.y * vx;
				ty = y;
				tx = x;

				if(y1>(opts.y+opts.height)){
					vy = (y-(opts.y+opts.height))/vector.y;
					x1 = x + Math.abs(vector.x * vy);
					y1 = opts.y+opts.height;
				}else if(y < opts.y){
					ty = opts.y;
					vy = (ty-y1)/vector.y;
					tx = x + opts.width - Math.abs(vy*vector.x);
				}

				sketchy.drawLine({
					svg:svg,
					x1:tx,
					y1:ty,
					x2:x1,
					y2:y1,
					sketch:{
						x:x_dist*opts.sketch,
						y:y_dist*opts.sketch
					}
				});

				y -= y_dist;
			}
		}

		return svg;
	};

	/**
	* draws a background pattern in the shape of a square according to the position and size of the clip-path object
	* @param {object} opts - object containing the attributes
	* @param {string} opts.clip - id of the clip path
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @param {float|int} opts.margin - extra margin for the background
	* @return {object} svg - d3.selection of a group object, containing the background
	*/
	sketchy.fill = function(opts){
		var merged_opts = extend(defaults, opts);

		var svg = merged_opts.svg.append("g")
			.attr("clip-path", "url(#"+merged_opts.clip+")");

		//Get the bounding box of the object that wants a background
		var bb = d3.select("#"+merged_opts.clip).node().getBBox();

		//To make sure that the background covers the whole are we increase the background by a few pixels
		merged_opts.x = bb.x-merged_opts.margin;
		merged_opts.y = bb.y-merged_opts.margin;
		merged_opts.width = bb.width + 2*merged_opts.margin;
		merged_opts.height = bb.height + 2*merged_opts.margin;
		merged_opts.svg = svg;

		return sketchy.drawPattern(merged_opts);
	};

	

	/**
	* draws a background pattern in the shape of a square according to the position and size of the clip-path object
	* @param {object} opts - object containing the attributes
	* @param {array} opts.path - array of points {x:float|integer, y:float|integer}
	* @param {int} opts.count - how many altered paths should be generated
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {array} paths - altered paths
	*/
	sketchy.alterPath = function(opts){
		var merged_opts = extend(defaults, opts);
		var paths = [];
		for(var i = 0; i<merged_opts.count; i++){
			var t_path = [];
			for(var j = 0; j<merged_opts.path.length; j++){
				t_path.push({
					x:merged_opts.path[j].x + rand(-1,1)*merged_opts.sketch/(i+1),
					y:merged_opts.path[j].y + rand(-1,1)*merged_opts.sketch/(i+1)
				});
			}
			paths.push(t_path);
		}
		return paths;
	};

	/**
	* Draws alterPath() paths
	* only straight lines, use alterPath and your own drawing function to draw curves etc.
	* @param {object} opts - object containing the attributes
	* @param {array} opts.svg - d3.selection of an svg
	* @param {array} opts.path - array of points {x:float|integer, y:float|integer}
	* @param {int} opts.count - how many altered paths should be generated
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {object} svg - svg with the strokes
	*/
	sketchy.pathStroke = function(opts){
		var paths = sketchy.alterPath(opts);
		var svg = opts.svg.append("g");
		for(var i = 0; i<paths.length; i++){
			for(var j = 0; j<paths[i].length; j++){
				var x1 = paths[i][j].x;
				var y1 = paths[i][j].y, x2, y2;
				if(j<(paths[i].length-1)){
					x2 = paths[i][j+1].x;
					y2 = paths[i][j+1].y;
				}else{
					x2 = paths[i][0].x;
					y2 = paths[i][0].y;
				}
				sketchy.drawLine({
					svg:svg,
					x1:x1,
					y1:y1,
					x2:x2,
					y2:y2,
					sketch:{
						x:opts.sketch,
						y:opts.sketch
					}
				});
			}
		}
		return svg;
	};

	/**
	* Helper function for circleStroke
	* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
	* Generates an altered circle path
	* @param {float|int} radius - radius of the circle
	* @param {float|int} radius_min - alternating radius min
	* @param {float|int} radius_max - alternating radius max
	* @param {float|int} s_angle_min - alternating angle min
	* @param {float|int} s_angle_max - alternating angle max
	* @param {float|int} rotation_min - alternating rotation min
	* @param {float|int} rotation_max - alternating rotation max
	* @return {string} path - altered circle svg path
	*/
	function circlePath(radius, radius_min,radius_max, s_angle_min, s_angle_max, rotation_min,rotation_max) {
		var c = 0.551915024494,
			b = Math.atan(c),
			d = Math.sqrt(c*c+1*1),
			r = radius,
			o = rand(s_angle_min, s_angle_max)*Math.PI/180,
			path = 'M';

		path += [r * Math.sin(o), r * Math.cos(o)];
		path += ' C' + [d * r * Math.sin(o + b), d * r * Math.cos(o + b)];

		for (var i=0; i<4; i++) {
			o += Math.PI/2 * (1 + rand(rotation_min, rotation_max));
			r *= (1 + rand(radius_min, radius_max));
			path += ' ' + (i?'S':'') + [d * r * Math.sin(o - b), d * r * Math.cos(o - b)];
			path += ' ' + [r * Math.sin(o), r * Math.cos(o)];
		}

		return path;
	}

	/**
	* Helper function for circleStroke
	* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
	* Generates the transform value for squashing and rotating
	* @param {float|int} squash_min - squashing min
	* @param {float|int} squash_max - squashing max
	* @param {float|int} squash_rotation_min - squashing rotation min
	* @param {float|int} squash_rotation_max - squashing rotation max
	* @return {string} path - transform string
	*/
	function circleTransform(squash_min, squash_max, squash_rotation_min, squash_rotation_max) {
		var o = rand(squash_rotation_min, squash_rotation_max);
		return 'rotate('+o+')'+'scale(1,'+rand(squash_min, squash_max) + ')';
	}

	/**
	* Draw a sketch circle stroke
	* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
	* @param {object} opts - object containing the attributes
	* @param {object} opts.svg - svg container
	* @param {float|int} opts.x - center x of circle
	* @param {float|int} opts.y - center y of circle
	* @param {float|int} opts.r - radius of circle
	* @param {int} count - number of strokes
	* @param {float|int} sketch - sketchiness factor
	* @return {object} svg - d3.selection of the svg containing the circles
	*/
	sketchy.circleStroke = function(opts){
		var merged_opts = extend(defaults, opts);

		var svg =  merged_opts.svg.append("g").attr('transform', function() { return "translate("+merged_opts.x+" "+merged_opts.y+") "+circleTransform(1,1, 0,360); });

		for(var i = 0; i<merged_opts.count; i++){
			svg.append('path')
				.attr('d', function() {
					return circlePath(merged_opts.r, merged_opts.sketch/-50/(i+1),merged_opts.sketch/10/(i+1), 200,240, 0,merged_opts.sketch/5/(i+1));
				});
		}

		return svg;
	};

	/**
	* Draw a sketch rectangle stroke
	* @param {object} opts - object containing the attributes
	* @param {object} opts.svg - svg container
	* @param {float|int} opts.x - x coordinate
	* @param {float|int} opts.y - y coordinate
	* @param {float|int} opts.width - width
	* @param {float|int} opts.height - height
	* @param {int} count - number of strokes
	* @param {float|int} sketch - sketchiness factor
	* @return {object} svg - d3.selection of the svg containing the rectangles
	*/
	sketchy.rectStroke = function(opts){
		var merged_opts = extend(defaults, opts);
		var svg = merged_opts.svg.append("g");


		var path = [
			{x:merged_opts.x, y:merged_opts.y},
			{x:merged_opts.x+merged_opts.width, y:merged_opts.y},
			{x:merged_opts.x+merged_opts.width, y:merged_opts.y+merged_opts.height},
			{x:merged_opts.x, y:merged_opts.y+merged_opts.height}
		];

		return sketchy.pathStroke({svg:svg, path:path, count:merged_opts.count, sketch:merged_opts.sketch});
	};

	return sketchy;
};