I saw a pretty cool post on reddit the other day and I thought it would be fun to recreate it using canvas and D3. Here it is!
Last active
January 25, 2019 21:35
-
-
Save pbeshai/da904ef5fd7f451e04e3b04568fef270 to your computer and use it in GitHub Desktop.
Line Circle Illusion
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit | |
height: 540 | |
border: no |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function colorScale(e){return rawColorScale(colorScaleMapper(e))}function createCircles(){for(var e=[],t=0;t<=maxCirclePower;t++)e=e.concat(createCirclePower(t));return e}function createCirclePower(e){var t=Math.pow(2,e),r=Math.pow(2,Math.max(0,e-1)),a=d3.range(r).map(function(e){var r=angleScale((2*e+1)/t);return createCircle(r)});return a}function createCircle(e){var t=.5,r=positionScale(e),a=t*Math.cos(e)+t,o=t*Math.sin(e)+t,c=t*Math.cos(e+Math.PI)+t,i=t*Math.sin(e+Math.PI)+t,l=d3.easeQuad(r),n=(1-l)*a+l*c,s=(1-l)*o+l*i;return{tPosition:r,direction:1,color:3*r,x:n,y:s,sx:a,sy:o,tx:c,ty:i}}function draw(e){ctx.save();for(var t,r=0;r<e.length&&r<numCirclesToShow;++r)t=e[r],ctx.beginPath(),ctx.arc(xScale(t.x),yScale(t.y),radius,0,tau),ctx.fillStyle=colorScale(t.color),ctx.fill(),ctx.closePath();ctx.restore()}function update(e){for(var t,r=0;r<e.length;++r){t=e[r],t.tPosition=Math.max(0,Math.min(t.tPosition+tickAmount*t.direction,1)),1!==t.tPosition&&0!==t.tPosition||(t.direction*=-1);var a=d3.easeQuad(t.tPosition);t.x=(1-a)*t.sx+a*t.tx,t.y=(1-a)*t.sy+a*t.ty,t.color=t.color+tickAmount,t.color+1e-6>3&&(t.color=0)}}function updateConfig(){radius=+d3.select("#radius").node().value,plotAreaWidth=width-2*radius,plotAreaHeight=height-2*radius,xScale.range([0,plotAreaWidth]),yScale.range([0,plotAreaHeight]),tickAmount=+d3.select("#tickAmount").node().value,colorInterpolator=d3[d3.select("#colorInterpolator").node().value],rawColorScale.interpolator(colorInterpolator),ctx.restore(),ctx.save(),ctx.translate(radius,radius),ctx.clearRect(-radius,-radius,width,height)}var tau=2*Math.PI,width=400,height=400,radius=10,plotAreaWidth=width-2*radius,plotAreaHeight=height-2*radius,tickAmount=.01,maxCirclePower=9,circlePower=0,numCirclesToShow=Math.pow(2,circlePower),numLoopsAtMax=4,resetCountdown=numLoopsAtMax,colorInterpolator=d3.interpolateRainbow,screenScale=window.devicePixelRatio||1,canvas=d3.select("canvas").attr("width",width*screenScale).attr("height",height*screenScale).style("width",width+"px").style("height",height+"px"),ctx=canvas.node().getContext("2d");ctx.scale(screenScale,screenScale),ctx.save(),ctx.translate(radius,radius);var xScale=d3.scaleLinear().domain([0,1]).range([0,plotAreaWidth]),yScale=d3.scaleLinear().domain([0,1]).range([0,plotAreaHeight]),angleScale=d3.scaleLinear().domain([0,1]).range([0,Math.PI]),positionScale=d3.scaleLinear().domain([0,Math.PI]).range([1,0]),colorScaleMapper=d3.scaleLinear().domain([0,1,1.5,2,3]).range([0,.6666666667,1,.666666667,0]),rawColorScale=d3.scaleSequential(colorInterpolator),circles=createCircles();draw(circles);var timer=d3.timer(function(e){update(circles),draw(circles),0===circles[0].tPosition&&(circlePower<maxCirclePower?(circlePower+=1,numCirclesToShow=Math.pow(2,circlePower)):0===resetCountdown?(circlePower=0,numCirclesToShow=Math.pow(2,circlePower),resetCountdown=numLoopsAtMax,ctx.clearRect(-radius,-radius,width,height)):resetCountdown-=1,console.log("Showing "+numCirclesToShow+" circles. Reset Countdown = "+resetCountdown))});d3.select(".controls").selectAll("input, select").on("change",updateConfig); | |
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJjb2xvclNjYWxlIiwidCIsInJhd0NvbG9yU2NhbGUiLCJjb2xvclNjYWxlTWFwcGVyIiwiY3JlYXRlQ2lyY2xlcyIsImxldCIsImNpcmNsZXMiLCJpIiwibWF4Q2lyY2xlUG93ZXIiLCJjb25jYXQiLCJjcmVhdGVDaXJjbGVQb3dlciIsInBvd2VyIiwiY29uc3QiLCJkZW5vbWluYXRvciIsIk1hdGgiLCJwb3ciLCJudW1DaXJjbGVzVG9BZGQiLCJtYXgiLCJjaXJjbGVSaW5nIiwiZDMiLCJyYW5nZSIsIm1hcCIsImFuZ2xlIiwiYW5nbGVTY2FsZSIsImNyZWF0ZUNpcmNsZSIsImJhc2VSYWRpdXMiLCJ0UG9zaXRpb24iLCJwb3NpdGlvblNjYWxlIiwic3giLCJjb3MiLCJzeSIsInNpbiIsInR4IiwiUEkiLCJ0eSIsImVhc2VRdWFkIiwieCIsInkiLCJkaXJlY3Rpb24iLCJjb2xvciIsImRyYXciLCJjdHgiLCJzYXZlIiwiY2lyY2xlIiwibGVuZ3RoIiwibnVtQ2lyY2xlc1RvU2hvdyIsImJlZ2luUGF0aCIsImFyYyIsInhTY2FsZSIsInlTY2FsZSIsInJhZGl1cyIsInRhdSIsImZpbGxTdHlsZSIsImZpbGwiLCJjbG9zZVBhdGgiLCJyZXN0b3JlIiwidXBkYXRlIiwibWluIiwidGlja0Ftb3VudCIsInVwZGF0ZUNvbmZpZyIsInNlbGVjdCIsIm5vZGUiLCJ2YWx1ZSIsInBsb3RBcmVhV2lkdGgiLCJ3aWR0aCIsInBsb3RBcmVhSGVpZ2h0IiwiaGVpZ2h0IiwiY29sb3JJbnRlcnBvbGF0b3IiLCJpbnRlcnBvbGF0b3IiLCJ0cmFuc2xhdGUiLCJjbGVhclJlY3QiLCJjaXJjbGVQb3dlciIsIm51bUxvb3BzQXRNYXgiLCJyZXNldENvdW50ZG93biIsImludGVycG9sYXRlUmFpbmJvdyIsInNjcmVlblNjYWxlIiwid2luZG93IiwiZGV2aWNlUGl4ZWxSYXRpbyIsImNhbnZhcyIsImF0dHIiLCJzdHlsZSIsImdldENvbnRleHQiLCJzY2FsZSIsInNjYWxlTGluZWFyIiwiZG9tYWluIiwic2NhbGVTZXF1ZW50aWFsIiwidGltZXIiLCJlbGFwc2VkIiwiY29uc29sZSIsImxvZyIsInNlbGVjdEFsbCIsIm9uIl0sIm1hcHBpbmdzIjoiQUFrREEsUUFBU0EsWUFBV0MsR0FDbkIsTUFBT0MsZUFBY0MsaUJBQWlCRixJQUt2QyxRQUFTRyxpQkFFUixJQUFLQyxHQUREQyxNQUNLQyxFQUFJLEVBQUdBLEdBQUtDLGVBQWdCRCxJQUNwQ0QsRUFBVUEsRUFBUUcsT0FBT0Msa0JBQWtCSCxHQUc1QyxPQUFPRCxHQVlSLFFBQVNJLG1CQUFrQkMsR0FDMUJDLEdBQU1DLEdBQWNDLEtBQUtDLElBQUssRUFBRUosR0FDMUJLLEVBQWtCRixLQUFLQyxJQUFLLEVBQUVELEtBQUtHLElBQUssRUFBRU4sRUFBVSxJQUVwRE8sRUFBZUMsR0FBQ0MsTUFBTUosR0FBaUJLLElBQUksU0FBQWQsR0FDaERLLEdBQU1VLEdBQVFDLFlBQWMsRUFBS2hCLEVBQUssR0FBR00sRUFDekMsT0FBT1csY0FBYUYsSUFHckIsT0FBT0osR0FJUixRQUFTTSxjQUFhRixHQUNyQlYsR0FBTWEsR0FBYSxHQUNiQyxFQUFZQyxjQUFjTCxHQUd4Qk0sRUFBR0gsRUFBYVgsS0FBS2UsSUFBSVAsR0FBU0csRUFDbENLLEVBQUdMLEVBQWFYLEtBQUtpQixJQUFJVCxHQUFTRyxFQUNsQ08sRUFBR1AsRUFBYVgsS0FBS2UsSUFBSVAsRUFBUVIsS0FBT21CLElBQUlSLEVBQzVDUyxFQUFHVCxFQUFhWCxLQUFLaUIsSUFBSVQsRUFBUVIsS0FBT21CLElBQUlSLEVBRzdDeEIsRUFBS2tCLEdBQUNnQixTQUFTVCxHQUNmVSxHQUFLLEVBQUtuQyxHQUFLMkIsRUFBSTNCLEVBQUsrQixFQUN4QkssR0FBSyxFQUFLcEMsR0FBSzZCLEVBQUk3QixFQUFLaUMsQ0FFL0IsUUFDQ1IsVUFBQUEsRUFDQVksVUFBVyxFQUNYQyxNQUFtQixFQUFaYixFQUNQVSxFQUFBQSxFQUNBQyxFQUFBQSxFQUNB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SUFBRWhFLE9BQVEsRUFBRU4sS0FBT21CLEtBQzlETixjQUFrQlIsR0FBQ2dFLGNBQWNDLFFBQVMsRUFBRXRFLEtBQU9tQixLQUFHYixPQUFRLEVBQUksSUFJbEVqQixpQkFBcUJnQixHQUFDZ0UsY0FDMUJDLFFBQVEsRUFBRyxFQUFHLElBQUssRUFBRyxJQUN0QmhFLE9BQU8sRUFBRyxZQUFjLEVBQUcsV0FBYSxJQUdwQ2xCLGNBQWtCaUIsR0FBQ2tFLGdCQUFnQmxCLG1CQWdIbkM3RCxRQUFVRixlQUNoQm9DLE1BQUtsQyxRQUdMRCxJQUFJaUYsT0FBUW5FLEdBQUdtRSxNQUFNLFNBQUFDLEdBQ3BCL0IsT0FBT2xELFNBQ1BrQyxLQUFLbEMsU0FHd0IsSUFBekJBLFFBQVEsR0FBR29CLFlBQ1Y2QyxZQUFjL0QsZ0JBQ2pCK0QsYUFBZSxFQUNmMUIsaUJBQW1CL0IsS0FBS0MsSUFBSSxFQUFHd0QsY0FDRixJQUFuQkUsZ0JBQ1ZGLFlBQWMsRUFDZDFCLGlCQUFtQi9CLEtBQUtDLElBQUksRUFBR3dELGFBQy9CRSxlQUFpQkQsY0FDakIvQixJQUFJNkIsV0FBV3BCLFFBQVNBLE9BQVFjLE1BQU9FLFNBRXZDTyxnQkFBa0IsRUFFbkJlLFFBQVFDLElBQUksV0FBUzVDLGlCQUFFLCtCQUFnQjRCLGtCQXVCekN0RCxJQUFHeUMsT0FBTyxhQUFhOEIsVUFBVSxpQkFDL0JDLEdBQUcsU0FBVWhDIiwiZmlsZSI6InNjcmlwdC5qcyIsInNvdXJjZXNDb250ZW50IjpbImNvbnN0IHRhdSA9IE1hdGguUEkgKiAyO1xuXG5jb25zdCB3aWR0aCA9IDQwMDtcbmNvbnN0IGhlaWdodCA9IDQwMDtcbmxldCByYWRpdXMgPSAxMDtcbmxldCBwbG90QXJlYVdpZHRoID0gd2lkdGggLSAyICogcmFkaXVzO1xubGV0IHBsb3RBcmVhSGVpZ2h0ID0gaGVpZ2h0IC0gMiAqIHJhZGl1cztcblxuLy8gY29ycmVzcG9uZHMgdG8gc3BlZWQgb2YgY2lyY2xlcy4gY2FuIGJlIGZ1biB0byBwbGF5IHdpdGguXG5sZXQgdGlja0Ftb3VudCA9IDAuMDE7XG5cbi8vIHdlIGdldCAyXnBvd2VyIG51bWJlciBvZiBjaXJjbGVzIGRyYXduXG5jb25zdCBtYXhDaXJjbGVQb3dlciA9IDk7XG5sZXQgY2lyY2xlUG93ZXIgPSAwO1xubGV0IG51bUNpcmNsZXNUb1Nob3cgPSBNYXRoLnBvdygyLCBjaXJjbGVQb3dlcik7XG5cbi8vIGFmdGVyIHdlIGhhdmUgcmVhY2hlZCBtYXggcG93ZXIsIGhvdyBtYW55IGxvb3BzXG4vLyBiZWZvcmUgcmVzZXR0aW5nIHRvIDEgY2lyY2xlP1xuY29uc3QgbnVtTG9vcHNBdE1heCA9IDQ7XG5sZXQgcmVzZXRDb3VudGRvd24gPSBudW1Mb29wc0F0TWF4O1xuXG4vLyBzZXQgdGhlIGNvbG9yIHNjaGVtZSBoZXJlOlxubGV0IGNvbG9ySW50ZXJwb2xhdG9yID0gZDMuaW50ZXJwb2xhdGVSYWluYm93O1xuXG4vLyBjcmVhdGUgdGhlIGNhbnZhc1xuY29uc3Qgc2NyZWVuU2NhbGUgPSB3aW5kb3cuZGV2aWNlUGl4ZWxSYXRpbyB8fCAxO1xuY29uc3QgY2FudmFzID0gZDMuc2VsZWN0KCdjYW52YXMnKVxuICAuYXR0cignd2lkdGgnLCB3aWR0aCAqIHNjcmVlblNjYWxlKVxuICAuYXR0cignaGVpZ2h0JywgaGVpZ2h0ICogc2NyZWVuU2NhbGUpXG4gIC5zdHlsZSgnd2lkdGgnLCBgJHt3aWR0aH1weGApXG4gIC5zdHlsZSgnaGVpZ2h0JywgYCR7aGVpZ2h0fXB4YClcbmNvbnN0IGN0eCA9IGNhbnZhcy5ub2RlKCkuZ2V0Q29udGV4dCgnMmQnKTtcbmN0eC5zY2FsZShzY3JlZW5TY2FsZSwgc2NyZWVuU2NhbGUpO1xuY3R4LnNhdmUoKTtcbmN0eC50cmFuc2xhdGUocmFkaXVzLCByYWRpdXMpO1xuXG4vLyBwb3NpdGlvbiBzY2FsZXNcbmNvbnN0IHhTY2FsZSA9IGQzLnNjYWxlTGluZWFyKCkuZG9tYWluKFswLCAxXSkucmFuZ2UoWzAsIHBsb3RBcmVhV2lkdGhdKTtcbmNvbnN0IHlTY2FsZSA9IGQzLnNjYWxlTGluZWFyKCkuZG9tYWluKFswLCAxXSkucmFuZ2UoWzAsIHBsb3RBcmVhSGVpZ2h0XSk7XG5jb25zdCBhbmdsZVNjYWxlID0gZDMuc2NhbGVMaW5lYXIoKS5kb21haW4oWzAsIDFdKS5yYW5nZShbMCwgTWF0aC5QSV0pO1xuY29uc3QgcG9zaXRpb25TY2FsZSA9IGQzLnNjYWxlTGluZWFyKCkuZG9tYWluKFswLCBNYXRoLlBJXSkucmFuZ2UoWzEsIDBdKVxuXG4vLyBjb2xvcnMtLSB3ZSB3YW50IHRvIHJlcGVhdCBldmVyeSAzIHRyaXBzIHdoaWxlIGxvb3BpbmcgZnJvbSBvbmUgZW5kXG4vLyB0byB0aGUgb3RoZXIgYW5kIGJhY2sgdG8gdGhlIGJlZ2lubmluZy5cbmNvbnN0IGNvbG9yU2NhbGVNYXBwZXIgPSBkMy5zY2FsZUxpbmVhcigpXG5cdC5kb21haW4oWzAsIDEsIDEuNSwgMiwgM10pXG5cdC5yYW5nZShbMCwgMC42NjY2NjY2NjY3LCAxLCAwLjY2NjY2NjY2NywgMF0pXG5cbi8vIHQgZ29lcyBmcm9tIDAgdG8gMlxuY29uc3QgcmF3Q29sb3JTY2FsZSA9IGQzLnNjYWxlU2VxdWVudGlhbChjb2xvckludGVycG9sYXRvcik7XG5mdW5jdGlvbiBjb2xvclNjYWxlKHQpIHtcblx0cmV0dXJuIHJhd0NvbG9yU2NhbGUoY29sb3JTY2FsZU1hcHBlcih0KSlcbn1cblxuXG4vLyBjcmVhdGUgY2lyY2xlcyBieSBhZGRpbmcgaW4gbGV2ZWxzIGJhc2VkIG9uIHBvd2VycyBvZiAyXG5mdW5jdGlvbiBjcmVhdGVDaXJjbGVzKCkge1xuXHRsZXQgY2lyY2xlcyA9IFtdO1xuXHRmb3IgKGxldCBpID0gMDsgaSA8PSBtYXhDaXJjbGVQb3dlcjsgaSsrKSB7XG5cdFx0Y2lyY2xlcyA9IGNpcmNsZXMuY29uY2F0KGNyZWF0ZUNpcmNsZVBvd2VyKGkpKTtcblx0fVxuXG5cdHJldHVybiBjaXJjbGVzO1xufVxuXG4vKlxuICBjcmVhdGUgY2lyY2xlcyBmb3IgYSBnaXZlbiBwb3dlciBvZiAyLlxuICBlLmcuOlxuXHRwb3dlcj0wICAxLFxuXHRwb3dlcj0xICAxLzIsXG5cdHBvd2VyPTIgIDEvNCAzLzRcblx0cG93ZXI9MyAgMS84IDMvOCA1LzggNy84XG5cdHBvd2VyPTQgIDEvMTYgMy8xNiA1LzE2IDcvMTYgOS8xNiAxMS8xNlxuKi9cbmZ1bmN0aW9uIGNyZWF0ZUNpcmNsZVBvd2VyKHBvd2VyKSB7XG5cdGNvbnN0IGRlbm9taW5hdG9yID0gTWF0aC5wb3coMiwgcG93ZXIpO1xuXHRjb25zdCBudW1DaXJjbGVzVG9BZGQgPSBNYXRoLnBvdygyLCBNYXRoLm1heCgwLCBwb3dlciAtIDEpKVxuXG5cdGNvbnN0IGNpcmNsZVJpbmcgPSBkMy5yYW5nZShudW1DaXJjbGVzVG9BZGQpLm1hcCgoaSkgPT4ge1xuXHRcdGNvbnN0IGFuZ2xlID0gYW5nbGVTY2FsZSgoKDIgKiBpKSArIDEpIC8gZGVub21pbmF0b3IpO1xuXHRcdHJldHVybiBjcmVhdGVDaXJjbGUoYW5nbGUpO1xuXHR9KTtcblxuXHRyZXR1cm4gY2lyY2xlUmluZztcbn1cblxuLy8gY3JlYXRlIGEgc2luZ2xlIGNpcmNsZSBhdCBhIGdpdmVuIGFuZ2xlXG5mdW5jdGlvbiBjcmVhdGVDaXJjbGUoYW5nbGUpIHtcblx0Y29uc3QgYmFzZVJhZGl1cyA9IDAuNTtcblx0Y29uc3QgdFBvc2l0aW9uID0gcG9zaXRpb25TY2FsZShhbmdsZSk7XG5cblx0Ly8gdXNlIHBvbGFyIGNvb3JkaW5hdGVzXG5cdGNvbnN0IHN4ID0gYmFzZVJhZGl1cyAqIE1hdGguY29zKGFuZ2xlKSArIGJhc2VSYWRpdXM7XG5cdGNvbnN0IHN5ID0gYmFzZVJhZGl1cyAqIE1hdGguc2luKGFuZ2xlKSArIGJhc2VSYWRpdXM7XG5cdGNvbnN0IHR4ID0gYmFzZVJhZGl1cyAqIE1hdGguY29zKGFuZ2xlICsgTWF0aC5QSSkgKyBiYXNlUmFkaXVzO1xuXHRjb25zdCB0eSA9IGJhc2VSYWRpdXMgKiBNYXRoLnNpbihhbmdsZSArIE1hdGguUEkpICsgYmFzZVJhZGl1cztcblxuXHQvLyBsaW5lYXIgaW50ZXJwb2xhdGUgd2l0aCBxdWFkcmF0aWMgZWFzaW5nIChlYXNpbmcgc2hvdWxkIG1hdGNoIHVwZGF0ZSBmdW5jKVxuXHRjb25zdCB0ID0gZDMuZWFzZVF1YWQodFBvc2l0aW9uKTtcblx0Y29uc3QgeCA9ICgxIC0gdCkgKiBzeCArIHQgKiB0eDtcblx0Y29uc3QgeSA9ICgxIC0gdCkgKiBzeSArIHQgKiB0eTtcblxuXHRyZXR1cm4ge1xuXHRcdHRQb3NpdGlvbixcblx0XHRkaXJlY3Rpb246IDEsXG5cdFx0Y29sb3I6IHRQb3NpdGlvbiAqIDMsXG5cdFx0eCxcblx0XHR5LFxuXHRcdHN4LFxuXHRcdHN5LFxuXHRcdHR4LFxuXHRcdHR5LFxuXHR9O1xufVxuXG4vLyBkcmF3IGNpcmNsZXMgb24gc2NyZWVuXG5mdW5jdGlvbiBkcmF3KGNpcmNsZXMpIHtcblx0Y3R4LnNhdmUoKTtcblxuXHRsZXQgY2lyY2xlO1xuXHRmb3IgKGxldCBpID0gMDsgaSA8IGNpcmNsZXMubGVuZ3RoICYmIGkgPCBudW1DaXJjbGVzVG9TaG93OyArK2kpIHtcblx0XHRjaXJjbGUgPSBjaXJjbGVzW2ldO1xuXHRcdGN0eC5iZWdpblBhdGgoKTtcblx0XHRjdHguYXJjKHhTY2FsZShjaXJjbGUueCksIHlTY2FsZShjaXJjbGUueSksIHJhZGl1cywgMCwgdGF1KVxuXHRcdGN0eC5maWxsU3R5bGUgPSBjb2xvclNjYWxlKGNpcmNsZS5jb2xvcik7XG5cdFx0Y3R4LmZpbGwoKVxuXHRcdGN0eC5jbG9zZVBhdGgoKTtcblx0fVxuXG5cdGN0eC5yZXN0b3JlKCk7XG59XG5cbi8vIHVwZGF0ZSBjaXJjbGUgcG9zaXRpb25zXG5mdW5jdGlvbiB1cGRhdGUoY2lyY2xlcykge1xuXHRsZXQgY2lyY2xlO1xuXHRmb3IgKGxldCBpID0gMDsgaSA8IGNpcmNsZXMubGVuZ3RoOyArK2kpIHtcblx0XHRjaXJjbGUgPSBjaXJjbGVzW2ldO1xuXG5cdFx0Ly8gdXBkYXRlIHBvc2l0aW9uXG5cdFx0Y2lyY2xlLnRQb3NpdGlvbiA9IE1hdGgubWF4KDAsIE1hdGgubWluKGNpcmNsZS50UG9zaXRpb24gKyB0aWNrQW1vdW50ICogY2lyY2xlLmRpcmVjdGlvbiwgMSkpO1xuXHRcdGlmIChjaXJjbGUudFBvc2l0aW9uID09PSAxIHx8IGNpcmNsZS50UG9zaXRpb24gPT09IDApIHtcblx0XHRcdGNpcmNsZS5kaXJlY3Rpb24gKj0gLTE7XG5cdFx0fVxuXG5cdFx0Ly8gaW1wb3J0YW50IHRvIHVzZSBxdWFkcmF0aWMgZWFzaW5nIHRvIGdldCB0aGUgY2lyY2xlIGlsbHVzaW9uIHNoYXBlXG5cdFx0Y29uc3QgdCA9IGQzLmVhc2VRdWFkKGNpcmNsZS50UG9zaXRpb24pO1xuXHRcdGNpcmNsZS54ID0gKDEgLSB0KSAqIGNpcmNsZS5zeCArIHQgKiBjaXJjbGUudHg7XG5cdFx0Y2lyY2xlLnkgPSAoMSAtIHQpICogY2lyY2xlLnN5ICsgdCAqIGNpcmNsZS50eTtcblxuXHRcdC8vIHVwZGF0ZSBjb2xvclxuXHRcdGNpcmNsZS5jb2xvciA9IGNpcmNsZS5jb2xvciArIHRpY2tBbW91bnQ7XG5cblx0XHQvLyBmaXggcm91bmRpbmcgZXJyb3Jcblx0XHRpZiAoY2lyY2xlLmNvbG9yICsgMWUtNiA+IDMpIHtcblx0XHRcdGNpcmNsZS5jb2xvciA9IDA7XG5cdFx0fVxuXHR9XG59XG5cblxuLy8gY3JlYXRlIHRoZSBjaXJjbGVzIGFuZCBkcmF3IHRoZW1cbmNvbnN0IGNpcmNsZXMgPSBjcmVhdGVDaXJjbGVzKCk7XG5kcmF3KGNpcmNsZXMpO1xuXG4vLyBiZWdpbiBhIHRpbWVyIGZvciBkb3VibGluZyB0aGUgbnVtYmVyIG9mIGNpcmNsZXMgYXQgcmVndWxhciBpbnRlcnZhbHNcbmxldCB0aW1lciA9IGQzLnRpbWVyKChlbGFwc2VkKSA9PiB7XG5cdHVwZGF0ZShjaXJjbGVzKTtcblx0ZHJhdyhjaXJjbGVzKTtcblxuXHQvLyBpZiB0aGUgZmlyc3QgY2lyY2xlIGlzIGJhY2sgaW4gc3RhcnQgcG9zaXRpb24sIGFkZCBhIG5ldyByaW5nXG5cdGlmIChjaXJjbGVzWzBdLnRQb3NpdGlvbiA9PT0gMCkge1xuXHRcdGlmIChjaXJjbGVQb3dlciA8IG1heENpcmNsZVBvd2VyKSB7XG5cdFx0XHRjaXJjbGVQb3dlciArPSAxO1xuXHRcdFx0bnVtQ2lyY2xlc1RvU2hvdyA9IE1hdGgucG93KDIsIGNpcmNsZVBvd2VyKTtcblx0XHR9IGVsc2UgaWYgKHJlc2V0Q291bnRkb3duID09PSAwKSB7XG5cdFx0XHRjaXJjbGVQb3dlciA9IDA7XG5cdFx0XHRudW1DaXJjbGVzVG9TaG93ID0gTWF0aC5wb3coMiwgY2lyY2xlUG93ZXIpO1xuXHRcdFx0cmVzZXRDb3VudGRvd24gPSBudW1Mb29wc0F0TWF4O1xuXHRcdFx0Y3R4LmNsZWFyUmVjdCgtcmFkaXVzLCAtcmFkaXVzLCB3aWR0aCwgaGVpZ2h0KTtcblx0XHR9IGVsc2Uge1xuXHRcdFx0cmVzZXRDb3VudGRvd24gLT0gMTtcblx0XHR9XG5cdFx0Y29uc29sZS5sb2coYFNob3dpbmcgJHtudW1DaXJjbGVzVG9TaG93fSBjaXJjbGVzLiBSZXNldCBDb3VudGRvd24gPSAke3Jlc2V0Q291bnRkb3dufWApO1xuXHR9XG59KTtcblxuXG4vLyBhZGQgY29udHJvbHNcbmZ1bmN0aW9uIHVwZGF0ZUNvbmZpZygpIHtcblx0cmFkaXVzID0gK2QzLnNlbGVjdCgnI3JhZGl1cycpLm5vZGUoKS52YWx1ZTtcblx0cGxvdEFyZWFXaWR0aCA9IHdpZHRoIC0gMiAqIHJhZGl1cztcblx0cGxvdEFyZWFIZWlnaHQgPSBoZWlnaHQgLSAyICogcmFkaXVzO1xuICB4U2NhbGUucmFuZ2UoWzAsIHBsb3RBcmVhV2lkdGhdKTtcbiAgeVNjYWxlLnJhbmdlKFswLCBwbG90QXJlYUhlaWdodF0pO1xuXG5cdHRpY2tBbW91bnQgPSArZDMuc2VsZWN0KCcjdGlja0Ftb3VudCcpLm5vZGUoKS52YWx1ZTtcblx0Y29sb3JJbnRlcnBvbGF0b3IgPSBkM1tkMy5zZWxlY3QoJyNjb2xvckludGVycG9sYXRvcicpLm5vZGUoKS52YWx1ZV07XG5cdHJhd0NvbG9yU2NhbGUuaW50ZXJwb2xhdG9yKGNvbG9ySW50ZXJwb2xhdG9yKTtcblxuXHRjdHgucmVzdG9yZSgpO1xuXHRjdHguc2F2ZSgpO1xuXHRjdHgudHJhbnNsYXRlKHJhZGl1cywgcmFkaXVzKTtcblx0Y3R4LmNsZWFyUmVjdCgtcmFkaXVzLCAtcmFkaXVzLCB3aWR0aCwgaGVpZ2h0KTtcbn1cblxuZDMuc2VsZWN0KCcuY29udHJvbHMnKS5zZWxlY3RBbGwoJ2lucHV0LCBzZWxlY3QnKVxuXHQub24oJ2NoYW5nZScsIHVwZGF0ZUNvbmZpZyk7XG5cblxuIl19 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<title>Line Circle Illusion</title> | |
<style> | |
body { | |
padding: 30px; | |
font-family: sans-serif; | |
} | |
.controls { | |
margin-top: 30px; | |
} | |
label { | |
display: inline-block; | |
font-size: 12px; | |
text-transform: uppercase; | |
font-weight: 600; | |
margin-right: 25px; | |
} | |
input, select { | |
display: block; | |
margin-top: 3px; | |
} | |
input { | |
width: 50px; | |
} | |
</style> | |
<body> | |
<canvas></canvas> | |
<div class="controls"> | |
<label> | |
Radius | |
<input type="number" id="radius" value="10" minValue="1" maxValue="100" step="1" /> | |
</label> | |
<label> | |
Tick Amount | |
<input type="number" id="tickAmount" value="0.01" minValue="0.01" maxValue="0.2" step="0.01" /> | |
</label> | |
<label> | |
Color | |
<select id="colorInterpolator"> | |
<option value="interpolateRainbow">Rainbow</option> | |
<option value="interpolateMagma">Magma</option> | |
<option value="interpolateInferno">Inferno</option> | |
<option value="interpolateViridis">Viridis</option> | |
<option value="interpolateWarm">Warm</option> | |
<option value="interpolateCool">Cool</option> | |
</select> | |
</label> | |
</div> | |
<script src='https://d3js.org/d3.v4.min.js'></script> | |
<script src='dist.js'></script> | |
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const tau = Math.PI * 2; | |
const width = 400; | |
const height = 400; | |
let radius = 10; | |
let plotAreaWidth = width - 2 * radius; | |
let plotAreaHeight = height - 2 * radius; | |
// corresponds to speed of circles. can be fun to play with. | |
let tickAmount = 0.01; | |
// we get 2^power number of circles drawn | |
const maxCirclePower = 9; | |
let circlePower = 0; | |
let numCirclesToShow = Math.pow(2, circlePower); | |
// after we have reached max power, how many loops | |
// before resetting to 1 circle? | |
const numLoopsAtMax = 4; | |
let resetCountdown = numLoopsAtMax; | |
// set the color scheme here: | |
let colorInterpolator = d3.interpolateRainbow; | |
// create the canvas | |
const screenScale = window.devicePixelRatio || 1; | |
const canvas = d3.select('canvas') | |
.attr('width', width * screenScale) | |
.attr('height', height * screenScale) | |
.style('width', `${width}px`) | |
.style('height', `${height}px`) | |
const ctx = canvas.node().getContext('2d'); | |
ctx.scale(screenScale, screenScale); | |
ctx.save(); | |
ctx.translate(radius, radius); | |
// position scales | |
const xScale = d3.scaleLinear().domain([0, 1]).range([0, plotAreaWidth]); | |
const yScale = d3.scaleLinear().domain([0, 1]).range([0, plotAreaHeight]); | |
const angleScale = d3.scaleLinear().domain([0, 1]).range([0, Math.PI]); | |
const positionScale = d3.scaleLinear().domain([0, Math.PI]).range([1, 0]) | |
// colors-- we want to repeat every 3 trips while looping from one end | |
// to the other and back to the beginning. | |
const colorScaleMapper = d3.scaleLinear() | |
.domain([0, 1, 1.5, 2, 3]) | |
.range([0, 0.6666666667, 1, 0.666666667, 0]) | |
// t goes from 0 to 2 | |
const rawColorScale = d3.scaleSequential(colorInterpolator); | |
function colorScale(t) { | |
return rawColorScale(colorScaleMapper(t)) | |
} | |
// create circles by adding in levels based on powers of 2 | |
function createCircles() { | |
let circles = []; | |
for (let i = 0; i <= maxCirclePower; i++) { | |
circles = circles.concat(createCirclePower(i)); | |
} | |
return circles; | |
} | |
/* | |
create circles for a given power of 2. | |
e.g.: | |
power=0 1, | |
power=1 1/2, | |
power=2 1/4 3/4 | |
power=3 1/8 3/8 5/8 7/8 | |
power=4 1/16 3/16 5/16 7/16 9/16 11/16 | |
*/ | |
function createCirclePower(power) { | |
const denominator = Math.pow(2, power); | |
const numCirclesToAdd = Math.pow(2, Math.max(0, power - 1)) | |
const circleRing = d3.range(numCirclesToAdd).map((i) => { | |
const angle = angleScale(((2 * i) + 1) / denominator); | |
return createCircle(angle); | |
}); | |
return circleRing; | |
} | |
// create a single circle at a given angle | |
function createCircle(angle) { | |
const baseRadius = 0.5; | |
const tPosition = positionScale(angle); | |
// use polar coordinates | |
const sx = baseRadius * Math.cos(angle) + baseRadius; | |
const sy = baseRadius * Math.sin(angle) + baseRadius; | |
const tx = baseRadius * Math.cos(angle + Math.PI) + baseRadius; | |
const ty = baseRadius * Math.sin(angle + Math.PI) + baseRadius; | |
// linear interpolate with quadratic easing (easing should match update func) | |
const t = d3.easeQuad(tPosition); | |
const x = (1 - t) * sx + t * tx; | |
const y = (1 - t) * sy + t * ty; | |
return { | |
tPosition, | |
direction: 1, | |
color: tPosition * 3, | |
x, | |
y, | |
sx, | |
sy, | |
tx, | |
ty, | |
}; | |
} | |
// draw circles on screen | |
function draw(circles) { | |
ctx.save(); | |
let circle; | |
for (let i = 0; i < circles.length && i < numCirclesToShow; ++i) { | |
circle = circles[i]; | |
ctx.beginPath(); | |
ctx.arc(xScale(circle.x), yScale(circle.y), radius, 0, tau) | |
ctx.fillStyle = colorScale(circle.color); | |
ctx.fill() | |
ctx.closePath(); | |
} | |
ctx.restore(); | |
} | |
// update circle positions | |
function update(circles) { | |
let circle; | |
for (let i = 0; i < circles.length; ++i) { | |
circle = circles[i]; | |
// update position | |
circle.tPosition = Math.max(0, Math.min(circle.tPosition + tickAmount * circle.direction, 1)); | |
if (circle.tPosition === 1 || circle.tPosition === 0) { | |
circle.direction *= -1; | |
} | |
// important to use quadratic easing to get the circle illusion shape | |
const t = d3.easeQuad(circle.tPosition); | |
circle.x = (1 - t) * circle.sx + t * circle.tx; | |
circle.y = (1 - t) * circle.sy + t * circle.ty; | |
// update color | |
circle.color = circle.color + tickAmount; | |
// fix rounding error | |
if (circle.color + 1e-6 > 3) { | |
circle.color = 0; | |
} | |
} | |
} | |
// create the circles and draw them | |
const circles = createCircles(); | |
draw(circles); | |
// begin a timer for doubling the number of circles at regular intervals | |
let timer = d3.timer((elapsed) => { | |
update(circles); | |
draw(circles); | |
// if the first circle is back in start position, add a new ring | |
if (circles[0].tPosition === 0) { | |
if (circlePower < maxCirclePower) { | |
circlePower += 1; | |
numCirclesToShow = Math.pow(2, circlePower); | |
} else if (resetCountdown === 0) { | |
circlePower = 0; | |
numCirclesToShow = Math.pow(2, circlePower); | |
resetCountdown = numLoopsAtMax; | |
ctx.clearRect(-radius, -radius, width, height); | |
} else { | |
resetCountdown -= 1; | |
} | |
console.log(`Showing ${numCirclesToShow} circles. Reset Countdown = ${resetCountdown}`); | |
} | |
}); | |
// add controls | |
function updateConfig() { | |
radius = +d3.select('#radius').node().value; | |
plotAreaWidth = width - 2 * radius; | |
plotAreaHeight = height - 2 * radius; | |
xScale.range([0, plotAreaWidth]); | |
yScale.range([0, plotAreaHeight]); | |
tickAmount = +d3.select('#tickAmount').node().value; | |
colorInterpolator = d3[d3.select('#colorInterpolator').node().value]; | |
rawColorScale.interpolator(colorInterpolator); | |
ctx.restore(); | |
ctx.save(); | |
ctx.translate(radius, radius); | |
ctx.clearRect(-radius, -radius, width, height); | |
} | |
d3.select('.controls').selectAll('input, select') | |
.on('change', updateConfig); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment