block by renecnielsen fa188c5c4106105ffa8e

Spirograph drawer - Animating solid and dashed lines

Full Screen

This is a random Spirograph drawing script, used to explain and show how solid and dashed lines can be animated through D3 in my blog “Animated (dashed) lines in d3.js with Spirographs”

The shape of the spirograph is random and the optional dash pattern is random as well. You can use the slider to make the animation go faster (which in counter-intuitive form happens when you slide to the left) or slower (when you slide right. See it as number of seconds of the animation, left is low, right is high). Remove all the spirographs to start anew by pressing “reset”.

forked from nbremer‘s block: Spirograph drawer - Animating solid and dashed lines

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
	<title>Creating a Spirograph</title>

	<!-- D3.js -->
	<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
	
	<!-- Google fonts -->
	<link href='//fonts.googleapis.com/css?family=Open+Sans:400' rel='stylesheet' type='text/css'>
	
	<!-- Pym.js - iframe height handler for the Blog -->
	<script src="pym.min.js"></script>
	
	<!-- jQuery -->
	<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
	<!-- Latest compiled and minified CSS -->
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
	<!-- Latest compiled and minified JavaScript -->
	<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
	
	<style>
	
		body {
			font-family: 'Open Sans', sans-serif;
			font-size: 12px;
			font-weight: 400;
			fill: #575757;
			text-align: center;
			background: #101420;
		}
		
		.spirograph {
			fill: none;
			stroke-width: 1px;
		}
		
		.rangeDiv {
			color: #949494;
		}
		
	</style>
  </head>
  <body>

	<div class="container-fluid">
		<!-- The chart -->
		<div class="row">
			<div class="col-sm-12">
				<div id="chart"></div>
			</div>
		</div>
		<!-- The buttons -->
		<div class="row">
			<div class="col-sm-6 col-sm-offset-3" style="margin-top: 20px;">
				<div id="button" class="btn-group" data-toggle="buttons">
					<label id="addButton" class="btn btn-default"><input type="radio" class="btn-options"> Add spiro </label>
					<label id="addDashedButton" class="btn btn-default"><input type="radio" class="btn-options"> Add dashed spiro </label>
					<label id="resetButton" class="btn btn-default"><input type="radio" class="btn-options"> Reset </label>
				</div>
			</div>
		</div>
		<!-- The slider -->
		<div class="row">
			<div class="col-sm-6 col-sm-offset-3 rangeDiv" style="margin-top: 20px; margin-bottom: 20px;">
				Adjust the duration of the next Spirograph you draw (left: faster | right: slower)
				<input type="range" id="rangeSlider" min="1" max="120" step="1" value="20" onchange="updateSlider(this.value)">
			</div>
		</div>
	</div>	
	
    <script>
		////////////////////////////////////////////////////////////
		//////////////////////// Set-up ////////////////////////////
		////////////////////////////////////////////////////////////

		var screenWidth = $(window).innerWidth(),
			screenHeight = ( $(window).innerHeight() > 160 ? $(window).innerHeight() - 120 : screenWidth );
		    mobileScreen = (screenWidth > 500 ? false : true);
	
		var margin = {left: 10, top: 10, right: 10, bottom: 10},
			width = screenWidth - margin.left - margin.right - 30,
			height = (mobileScreen ? 300 : screenHeight) - margin.top - margin.bottom - 30;
			
		var maxSize = Math.min(width, height) / 2,
			drawDuration = 20;
			
		//d3.select("#rangeSlider").attr("value", drawDuration);
					
		var svg = d3.select("#chart").append("svg")
					.attr("width", (width + margin.left + margin.right))
					.attr("height", (height + margin.top + margin.bottom))
				  .append("g").attr("class", "wrapper")
					.attr("transform", "translate(" + (width / 2 + margin.left) + "," + (height / 2 + margin.top) + ")");

		var line = d3.svg.line()
			.x(function(d) { return d.x; })
			.y(function(d) { return d.y; });

		////////////////////////////////////////////////////////////
		//////////////////// Draw a Spirograph /////////////////////
		////////////////////////////////////////////////////////////
		var colors = ["#00AC93", "#EC0080", "#FFE763"];	
		var numColors = 3;
		var startColor = getRandomNumber(0,numColors); //Loop through the colors, but the starting color is random
		
		function addSpiro(doDash) {
			var path = svg.append("path")
				.attr("class", "spirograph")
				.attr("d", line(plotSpiroGraph()) )
				.style("stroke", colors[startColor]);
				//.style("stroke", "hsla(" + startColor/numColors * 360 + ", 100%, 50%, " + 0.9 + ")");	
				
			var totalLength = path.node().getTotalLength();	
			  
			if (doDash) {
				//Adjusted from //stackoverflow.com/questions/24021971/animate-the-drawing-of-a-dashed-svg-line
				//The first number specifies the length of the visible part, the dash, the second number specifies the length of the white part
				var dashing = getRandomNumber(1,10) + ", " + getRandomNumber(1,10);  //Something such as "6,6" could happen
				console.log("Dash pattern is: " + dashing);
				//This returns the length of adding all of the numbers in dashing (the length of one pattern in essense)
				//So for "6,6", for example, that would return 6+6 = 12
				var dashLength = 
					dashing
						.split(/[\s,]/)
						.map(function (a) { return parseFloat(a) || 0 })
						.reduce(function (a, b) { return a + b });
				//How many of these dash patterns will fit inside the entire path?
			    var dashCount = Math.ceil( totalLength / dashLength );
				//Create an array that holds the pattern as often so it will fill the entire path
			    var newDashes = new Array(dashCount).join( dashing + " " );
				//Then add one more dash pattern, namely with a visible part of length 0 (so nothing) and a white part
				//that is the same length as the entire path
				var dashArray = newDashes + " 0, " + totalLength;
			} else {
				//For a solid looking line, create a dash pattern with a visible part and a white part
				//that are the same length as the entire path
				var dashArray = totalLength + " " + totalLength;
			}
			
			//Animate the path by offsetting the path so all you see is the white last bit of dashArray 
			//(which has a length that is the same length as the entire path), and then slowly move this
			//out of the way so the rest of the path becomes visible (the stuff at the start of dashArray)
			path
			  	.attr("stroke-dasharray", dashArray)
			  	.attr("stroke-dashoffset", totalLength)
			  	.transition().duration(drawDuration * 1000).ease("linear")
				.attr("stroke-dashoffset", 0);
				
		}//function addSpiro

		////////////////////////////////////////////////////////////
		////////////////// Button Activity /////////////////////////
		////////////////////////////////////////////////////////////

		d3.select("#addButton").on("click", function () {
			//Create and draw a spiro
			addSpiro(false);
			startColor = (startColor+1)%numColors;
			//Make the button inactive again
			setTimeout( function() { d3.select("#addButton").classed("active", false); }, 200);
		});
		
		d3.select("#addDashedButton").on("click", function () {
			//Create and draw a dashed spiro
			addSpiro(true);
			startColor = (startColor+1)%numColors;
			//Make the button inactive again
			setTimeout( function() { d3.select("#addDashedButton").classed("active", false); }, 200);
		});
		
		d3.select("#resetButton").on("click", function () {
			//Remove all spiros
			d3.selectAll(".spirograph").remove();
			getRandomNumber(0,numColors);
			//Make the button inactive again
			setTimeout( function() { d3.select("#resetButton").classed("active", false); }, 200);
		});
		
		function updateSlider(value) {
			drawDuration = value;
		}
		
		//Start drawing one spirograph after 1 second after reload
		setTimeout(function() {
			addSpiro(false);
			startColor = (startColor+1)%numColors;
		}, 1000);
	
		////////////////////////////////////////////////////////////
		////////////////// Spirograph functions ////////////////////
		////////////////////////////////////////////////////////////
						
        function plotSpiroGraph() {
            //Function adjusted from: https://github.com/alpha2k/HTML5Demos/blob/master/Canvas/spiroGraph.html
			
            var R = getRandomNumber(60, maxSize);
            var r = getRandomNumber(40, (R * 0.75));
            var alpha = getRandomNumber(25, r);
            var l = alpha / r;
            var k = r / R;
            
            //Create the x and y coordinates for the spirograph and put these in a variable
			var lineData = [];
            for(var theta=1; theta<=20000; theta += 1){
                var t = ((Math.PI / 180) * theta);
                var ang = ((l-k)/k) * t;
                var x = R * ((1-k) * Math.cos(t) + ((l*k) * Math.cos(ang)));
                var y = R * ((1-k) * Math.sin(t) - ((l*k) * Math.sin(ang)));
				
                lineData.push({x: x, y: y});                               
            }  
			
			//Output the variables of this spiro         
			console.log("R: " + R + ", r: " + r + ", alpha: " + alpha + ", l: " + l + ", k: " + k);
			
			return lineData;
        }
				
        function getRandomNumber(start, end) {
            return (Math.floor((Math.random() * (end-start))) + start);
        }	
		
		//iFrame handler
		var pymChild = new pym.Child();
		pymChild.sendHeight();
		//setTimeout(function() { pymChild.sendHeight(); }, 2000);
		
	</script>
	
  </body>
</html>

pym.min.js

/*! pym.js - v0.4.4 - 2015-07-16 */
!function(a){"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&module.exports?module.exports=a():window.pym=a.call(this)}(function(){var a="xPYMx",b={},c=function(a){var b=new RegExp("[\\?&]"+a.replace(/[\[]/,"\\[").replace(/[\]]/,"\\]")+"=([^&#]*)"),c=b.exec(location.search);return null===c?"":decodeURIComponent(c[1].replace(/\+/g," "))},d=function(a,b){return"*"===b.xdomain||a.origin.match(new RegExp(b.xdomain+"$"))?!0:void 0},e=function(b,c,d){var e=["pym",b,c,d];return e.join(a)},f=function(b){var c=["pym",b,"(\\S+)","(.+)"];return new RegExp("^"+c.join(a)+"$")},g=function(){for(var a=document.querySelectorAll("[data-pym-src]:not([data-pym-auto-initialized])"),c=a.length,d=0;c>d;++d){var e=a[d];e.setAttribute("data-pym-auto-initialized",""),""===e.id&&(e.id="pym-"+d);var f=e.getAttribute("data-pym-src"),g=e.getAttribute("data-pym-xdomain"),h={};g&&(h.xdomain=g),new b.Parent(e.id,f,h)}};return b.Parent=function(a,b,c){this.id=a,this.url=b,this.el=document.getElementById(a),this.iframe=null,this.settings={xdomain:"*"},this.messageRegex=f(this.id),this.messageHandlers={},c=c||{},this._constructIframe=function(){var a=this.el.offsetWidth.toString();this.iframe=document.createElement("iframe");var b="",c=this.url.indexOf("#");c>-1&&(b=this.url.substring(c,this.url.length),this.url=this.url.substring(0,c)),this.url.indexOf("?")<0?this.url+="?":this.url+="&",this.iframe.src=this.url+"initialWidth="+a+"&childId="+this.id+"&parentUrl="+encodeURIComponent(window.location.href)+b,this.iframe.setAttribute("width","100%"),this.iframe.setAttribute("scrolling","no"),this.iframe.setAttribute("marginheight","0"),this.iframe.setAttribute("frameborder","0"),this.el.appendChild(this.iframe),window.addEventListener("resize",this._onResize)},this._onResize=function(){this.sendWidth()}.bind(this),this._fire=function(a,b){if(a in this.messageHandlers)for(var c=0;c<this.messageHandlers[a].length;c++)this.messageHandlers[a][c].call(this,b)},this.remove=function(){window.removeEventListener("message",this._processMessage),window.removeEventListener("resize",this._onResize),this.el.removeChild(this.iframe)},this._processMessage=function(a){if(d(a,this.settings)&&"string"==typeof a.data){var b=a.data.match(this.messageRegex);if(!b||3!==b.length)return!1;var c=b[1],e=b[2];this._fire(c,e)}}.bind(this),this._onHeightMessage=function(a){var b=parseInt(a);this.iframe.setAttribute("height",b+"px")},this._onNavigateToMessage=function(a){document.location.href=a},this.onMessage=function(a,b){a in this.messageHandlers||(this.messageHandlers[a]=[]),this.messageHandlers[a].push(b)},this.sendMessage=function(a,b){this.el.getElementsByTagName("iframe")[0].contentWindow.postMessage(e(this.id,a,b),"*")},this.sendWidth=function(){var a=this.el.offsetWidth.toString();this.sendMessage("width",a)};for(var g in c)this.settings[g]=c[g];return this.onMessage("height",this._onHeightMessage),this.onMessage("navigateTo",this._onNavigateToMessage),window.addEventListener("message",this._processMessage,!1),this._constructIframe(),this},b.Child=function(b){this.parentWidth=null,this.id=null,this.parentUrl=null,this.settings={renderCallback:null,xdomain:"*",polling:0},this.messageRegex=null,this.messageHandlers={},b=b||{},this.onMessage=function(a,b){a in this.messageHandlers||(this.messageHandlers[a]=[]),this.messageHandlers[a].push(b)},this._fire=function(a,b){if(a in this.messageHandlers)for(var c=0;c<this.messageHandlers[a].length;c++)this.messageHandlers[a][c].call(this,b)},this._processMessage=function(a){if(d(a,this.settings)&&"string"==typeof a.data){var b=a.data.match(this.messageRegex);if(b&&3===b.length){var c=b[1],e=b[2];this._fire(c,e)}}}.bind(this),this._onWidthMessage=function(a){var b=parseInt(a);b!==this.parentWidth&&(this.parentWidth=b,this.settings.renderCallback&&this.settings.renderCallback(b),this.sendHeight())},this.sendMessage=function(a,b){window.parent.postMessage(e(this.id,a,b),"*")},this.sendHeight=function(){var a=document.getElementsByTagName("body")[0].offsetHeight.toString();this.sendMessage("height",a)}.bind(this),this.scrollParentTo=function(a){this.sendMessage("navigateTo","#"+a)},this.navigateParentTo=function(a){this.sendMessage("navigateTo",a)},this.id=c("childId")||b.id,this.messageRegex=new RegExp("^pym"+a+this.id+a+"(\\S+)"+a+"(.+)$");var f=parseInt(c("initialWidth"));this.parentUrl=c("parentUrl"),this.onMessage("width",this._onWidthMessage);for(var g in b)this.settings[g]=b[g];return window.addEventListener("message",this._processMessage,!1),this.settings.renderCallback&&this.settings.renderCallback(f),this.sendHeight(),this.settings.polling&&window.setInterval(this.sendHeight,this.settings.polling),this},g(),b});