JSFiddle - React, Tailwind, and code Playground

HTML

<script type="text/javascript">

// Simulation...
var EPI;

function syncValues() {
	var nr = parseInt( $('#nr').val() ),
		n1 = parseInt( $('#p1s').val() ),
		n2 = parseInt( $('#p2s').val() ),
		r1 = parseInt( $('#p1r').val() ),
		r2 = parseInt( $('#p2r').val() );

	$('#nrv').html( nr );
	$('#p1sv').html( n1 );
	$('#p2sv').html( n2 );
	$('#p1rv').html( r1 );
	$('#p2rv').html( r2 );
}

function reEpi() {
	var nr = parseInt( $('#nr').val() ),
		n1 = parseInt( $('#p1s').val() ),
		n2 = parseInt( $('#p2s').val() ),
		r1 = parseInt( $('#p1r').val() ),
		r2 = parseInt( $('#p2r').val() );

	syncValues();
	$('#rto').val( 0 );
	EPI = epi( nr, r1, r2, n1, n2 );
	epiRender();
}

function epiRender() {
	var rTo = $('#rto').val()/500,
		nr = $('#nr').val();

	$('#rtov').html( ~~(rTo*nr*360) + '&deg;' );
	EPI( $('canvas')[0], rTo*nr*2*Math.PI, $('#ccw')[0].checked,
		($('#drawp1')[0].checked ? 1 : 0) + ($('#drawtr')[0].checked ? 2 : 0) + ($('#drawp2')[0].checked ? 4 : 0) );
}

$( function() {
	$('.re-epi').bind( 'change', reEpi ).bind( 'input', syncValues );
	$('#rto').bind( 'input', epiRender );
	$('.re-render').bind( 'change', epiRender );
	$('input[type="range"]').css( 'width', '500px' );
	reEpi();
	epiRender();
	setInterval( function() {
		var $R2 = $('#rto');
		if( $('#anim')[0].checked ) {
			$R2.val( (parseInt($R2.val())+1) %500 );
			epiRender();
		}
	}, 50 );
} );

</script>
<body>
<div align="center">
<canvas width="600" height="600"></canvas>
<table>
<tr><td><label for="nr">Number of Rotations: </label></td><td><input class="re-epi" id="nr" type="range" min="1" max="5" value="2" /><span style="display:inline-block; min-width:40px;" id="nrv"></span></td></tr>
<tr><td><label for="p1s">Sides in Polygon 1: </label></td><td><input class="re-epi" id="p1s" type="range" min="2" max="60" value="4" /><span id="p1sv"></span></td></tr>
<tr><td><label for="p1r">Radius of Polygon 1: </label></td><td><input class="re-epi" id="p1r" type="range" min="5" max="200" value="60" /><span...

JavaScript

function epi(B,r2,r1,n2,n1){K=Math;function C(t){return K.cos(t)}function S(t){return K.sin(t)}function A(y,x){return K.atan2(y,x)}P=K.PI;v=[[],[]];w=[[],[]];z=[];function Z(x,y,j){c=C(t=f*H+P/2);s=S(t);v[j][n]=c*x-s*y;w[j][n]=s*x+c*y;}function E(i){return{x:r1*S(t=p-i*q),y:r1*C(t)};}function D(x,y,X,Y,t){L=A(m.y,m.x);M=K.sqrt(m.x*m.x+m.y*m.y);N=K.sqrt(X*X+Y*Y);O=~~(t*(M>N?M:N)+1);for(i=J;i<=O;i++){J=1;z[n]=f*H+P+t*i/O;Z(x+M*C(T=L+t*i/O),y+M*S(T),0);Z(x+N*C(T=A(Y,X)+t*i/O),y+N*S(T),1);n++}}function F(x,y,n,r,L,s){I.strokeStyle=s;I.beginPath();for(i=0;i<n;i++)I[i?'lineTo':'moveTo'](x+r*C(t=L+(1-2*i)*P/n),y+r*S(t)*W);I.closePath();I.stroke()}p=P/n1;q=2*p;u=P/n2;H=2*u;s2=r2*S(u);g=f=l=n=J=0;h=1;R=300;while(l<=(B*2+1)*P/H){o=E(0);m=E(h);m.y-=o.y;m.x-=o.x;if(g<s2){D(g,-r2*C(u),-o.x,-o.y,q);h=(h+1)%n1;g+=2*r1*S(p)}else{m.x+=g-s2;D(s2,-r2*C(u),-o.x+g-s2,-o.y,H);g-=s2*2;f=(f+1)%n2;l++}}return function(e_,t,aa,W_){W=aa?-1:1;I=(e=e_).getContext('2d');I.fillStyle='black';I.fillRect(0,0,600,600);W_&1&&F(R,R,n2,r2,0,'white');T=A(w[1][0],v[1][0]);U=V=0;I.strokeStyle='teal';I.beginPath();I.moveTo(R+v[0][0],R+w[0][0]*W);while(U<t){_=A(w[1][V+1],v[1][V+1]);U+=_-T+(_+1<T?2*P:0);T=_;V++;I.lineTo(R+v[0][V],R+w[0][V]*W)}W_&2&&I.stroke();W_&4&&F(R+v[1][V],R+w[1][V]*W,n1,r1,z[V],'red')}}