Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active January 25, 2019 21:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pbeshai/da904ef5fd7f451e04e3b04568fef270 to your computer and use it in GitHub Desktop.
Save pbeshai/da904ef5fd7f451e04e3b04568fef270 to your computer and use it in GitHub Desktop.
Line Circle Illusion
license: mit
height: 540
border: no
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
<!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>
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