// Note: JavaScript computes angles in radians, while SVGs compute angles in degrees. /////////////////////////////////////////////////////////////////////////////// // Parameters /////////////////////////////////////////////////////////////////////////////// // Canvas dimensions var width = 500; var height = 500; // Animation parameters var startingAngle = 30; var angleCycle = 90; var animateLength = 1500; // Milliseconds. // Shape paraeters var count = 6; // Parameters for shape 1: pie var r = 50; // Radius of circle var eyeR = 7.5; // Radius of 'eye' var cx = 5; // Offset from center of pie var cy = 20; // Offset from center of pie // Parameters for shape 2: angle var l = 60; // Length of triangle side. var θ = Math.PI / 3; // Angle in radians var SECRET = true; /////////////////////////////////////////////////////////////////////////////// // Derived values (do not change) /////////////////////////////////////////////////////////////////////////////// var exposedColor = SECRET? "#FFEE00" : "#000000"; // #F4EC30? var centerX = width / 2; var centerY = height / 2; var radius = Math.min(centerX, centerY) / 2; var polySides = count / 2; // Requires count be even. var innerAngle = 2 * Math.PI / polySides; var halfInnerAngle = innerAngle / 2; // Pie angle (slice) = value of inside angle. // Sum of inside angles = 180 + 180(n - 3) = 180(n - 2) // Value of one inside angle = 180(n - 2)/n = 180 - 360/n // (Well, that in radians.) var cθ = Math.PI - 2 * Math.PI / polySides; // Angle offsets from edge of circle: var dx = radius * (Math.cos(halfInnerAngle) - 1); // = -(radius - Math.cos(halfInnerAngle) * radius); var dy = 0; // Unused, but here for completeness. /////////////////////////////////////////////////////////////////////////////// // SVG elements /////////////////////////////////////////////////////////////////////////////// // Generates pie shape (without 'eye'). // Approximately looks like this: /* y | -->)-- x | */ var pie = d3.svg.arc() .innerRadius(0) .outerRadius(r) .startAngle(cθ / 2 - Math.PI / 2) .endAngle(2 * Math.PI - cθ / 2 - Math.PI / 2) // 2 Pi needed to go around. ; // Generates angle shape. // Approximately looks like this: /* y |\ | \ 0 -- x | / |/ */ var angle = function() { // For some reason, using direct ratios creates less accurate angles, // so I'll be using trigonometry. var point1 = [0, - Math.sin(θ / 2) * l]; var point2 = [Math.cos(θ / 2) * l, 0]; var point3 = [0, Math.sin(θ / 2) * l]; var points = [point1, point2, point3]; points.forEach(function(e) { e[0] = e[0] + dx; e[1] = e[1] + dy; e = e.join(" "); }); var path = "M" + points.join(" L"); return path; }(); // Self-executing anonymous function. /////////////////////////////////////////////////////////////////////////////// // Helper functions /////////////////////////////////////////////////////////////////////////////// // Eschew needless annoying string concatenation. function translate(x, y) {return "translate(" + x + "," + y + ")";} function rotate(angle) {return "rotate(" + angle + ")";} function rotateAroundPoint(angle, x, y) {return "rotate(" + angle + "," + x + ", " + y + ")";} // Convert the index of an element to an angle on a circle. function toRadians(i) { var tau = 2 * Math.PI; var ratio = i / count; return tau * ratio; } function toDegrees(i) { var degrees = 360; var ratio = i / count; return degrees * ratio; } // Determine the output X and Y coordinates relative to (0, 0) of its parent. function getX(i) {return radius * Math.cos(toRadians(i));} // centerX function getY(i) {return radius * Math.sin(toRadians(i));} // centerY function getOppositeX(i) {return - radius * Math.cos(toRadians(i));} // centerX function getOppositeY(i) {return - radius * Math.sin(toRadians(i));} // centerY /////////////////////////////////////////////////////////////////////////////// // Graphics generation /////////////////////////////////////////////////////////////////////////////// // Create a dummy array to 'store' each shape. var data = new Array(count); // Create the canvas to place our images. var svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height) ; // Create a group selection to center and rotate the shapes. var all = svg.append("g") .attr("transform", translate(centerX, centerY)); // Creates a data join selection (this is the confusing part). var join = all.selectAll("g").data(data); // For every element that was passed into data(), we do the following things: var gEnter = join.enter() // Create a sub-group and place it accordingly around the center. .append("g") .attr("transform", function(d, i) { var t = translate(getX(i), getY(i)); var r = rotateAroundPoint(toDegrees(i), 0, 0); return t + r; }) ; // Create the shape corresponding to the group. gEnter.each(function(d, i) { var elem = d3.select(this); if (i % 2 === 0) { elem.append("path") .attr("d", angle) .attr("id", "angle") .attr("stroke", SECRET? "none" : "black") .attr("fill", SECRET? exposedColor : "none") ; } else { elem.append("path") .attr("d", pie) .attr("id", "pie") .attr("fill", exposedColor) ; elem.append("circle") .attr("r", eyeR) .attr("id", "eye") .attr("cx", cx) .attr("cy", cy) ; } }) /////////////////////////////////////////////////////////////////////////////// // Animation /////////////////////////////////////////////////////////////////////////////// // Animate the shapes, rotating the canvas as an offset of the previous animation. function animateForward(prevAngle) { // newAngle will overflow after a long time, but I don't think we should worry about that. var newAngle = prevAngle + angleCycle; // Rotate the entire image. all.transition().duration(animateLength) .attr("transform", translate(centerX, centerY) + rotate(newAngle)) // Mutually recursive call to animateBackward .each("end", function(){animateBackward(newAngle);}) ; // Move each shape to the opposite side of the circle. join.transition().duration(animateLength) .attr("transform", function(d, i) { var transforms = d3.transform(d3.select(this).attr("transform")); var t = translate(getOppositeX(i), getOppositeY(i)); var r = rotateAroundPoint(transforms.rotate, 0, 0); return t + r; }) ; } function animateBackward(prevAngle) { var newAngle = prevAngle + angleCycle; // Rotate the entire image. all.transition().duration(animateLength) .attr("transform", translate(centerX, centerY) + rotate(newAngle)) // Mutually recursive call to animateForward .each("end", function(){animateForward(newAngle);}) ; // Move each shape to the original side of the circle. join.transition().duration(animateLength) .attr("transform", function(d, i) { var transforms = d3.transform(d3.select(this).attr("transform")); var t = translate(getX(i), getY(i)); var r = rotateAroundPoint(transforms.rotate, 0, 0); return t + r; }) ; } /////////////////////////////////////////////////////////////////////////////// // Audio /////////////////////////////////////////////////////////////////////////////// loop = new SeamlessLoop(); // From the SeamlessLoop library. loop.addUri("http://a.uguu.se/mgtczq.wav", 3000, "loop"); /////////////////////////////////////////////////////////////////////////////// // Play /////////////////////////////////////////////////////////////////////////////// function start() { if (SECRET) {loop.start("loop");} // Start playing the audio loop. animateForward(startingAngle); // Run the animation at the same time. } loop.callback(start); /////////////////////////////////////////////////////////////////////////////// // Interact /////////////////////////////////////////////////////////////////////////////// var recolor = function() { d3.selectAll("#angle") .attr("stroke", SECRET? "none" : "black") .attr("fill", SECRET? exposedColor : "none") ; d3.selectAll("#pie") .attr("fill", exposedColor) ; } var reloop = function() { if (SECRET) {loop.start("loop");} else {loop.stop("loop");} } var toggleSecret = function() { SECRET = !SECRET; exposedColor = SECRET? "#FFEE00" : "#000000"; recolor(); reloop(); } svg.on("click", toggleSecret);