"JS1K" drawn by 64 gears. Press up / down to change the number of gears, left to show/hide intermediate paths, right to clear the scene. Use the mouse to draw a (closed) path!
for(_='[i44]:f(?/2Ja.heightJL,e.y-LYa.widthje.x-jJqc[c.zzsS]=`~e=>Fdown=FE+P4+1]Oc.arc(N+=V0,UMath.hypot(for( in C,H);].length-p[p-1]](*sin*cos;i of&&P[k][0](i*T+i[2=[]Ponmouse G){zbac.stroke(50*(i.charCodeAt().push([Z[g][1]P[(I+J)%][(k*-2*PI*i/)ic)c4+4[6]]]=i;p;m=0`@DGCBCGDPHIRSKWTQTW[XZ]Z_Z[SRIHDD`)p>>2)-95U&3)-90 Em=1; move=Fm?e up=F(m=U?e)onkeyE[FD^=1,FQ++,F(p,G,?)),FQ--][e.which-37f=F{e(!p||qY)>9)pqYP=p.map(F[...eZb.bgColor=i=D=T=0;>2**(log2()|0iV2)P.splice(i+1,U[(P:O)J,(P:O)J_iP){_:=[U0]kP)_:V(-),_:V(+}P=_;i?)I=,G;I--;)GI+J-,0],1])/,atan2(1],0])G.sort((i,B)=>B-iQ=-1};?setInterval(F{TV.007;j^=C=H=g=0;ztajJ,L~#fff` p)zba),Ni,i,1,U7),)N,i,U7zmCViHVizlg<=Q=||[];<999g++}g=0zld]=5,~hsl(${(Q-g)*9},50%,50%`,zm,)K of )(g==min(Q,-1)||Dgg<=Qg%8<1)zlK,Kg++}},9)';G=/[-UVNOEF~zqjYLJ?:4]/.exec(_);)with(_.split(G))_=join(shift());eval(_)
Zm9yKF89J1tpNDRdOmYoPy8ySmEuaGVpZ2h0SkwsZS55LUxZYS53aWR0aGplLngtakpxY1tjLnp6c1NdPWB+ZT0+RmRvd249RkUrUDQrMV1PYy5hcmMoTis9VjAsVU1hdGguHx9oeXBvdCgeZm9yKB0gaW4gHEMsSBspOxpdGhkubGVuZ3RoGC1wW3AYLTFdF10oFiofc2luFSofY29zFDsdExNpIG9mEiYmEVBba10QWzBdDyhpDypUK2lbMhkOPVtdDFAYC29ubW91c2UJEiBHKXt6YmEWGghjLnN0cm9rZSgHNTAqKGkuY2hhckNvZGVBdCgpBi5wdXNoKFsFWltnXQRbMV0DUFsoSSsLSiklC11bAihrKi0yKh9QSSppLwspAR1pHGMpYzQPKzRbNl1dXT1pO3AMO209MBJgQERHQ0JDR0RQSElSU0tXVFFUV1tYWl1aX1pbU1JJSEREYClwBQY+PjIpLTk1VQYmMyktOTAZCUVtPTE7CW1vdmU9Rm0RP2UaCXVwPUYobT1VP2UpGm9ua2V5RVtGRF49MSxGUSsrLEYocAwsRwwsPykpLEZRLS1dW2Uud2hpY2gtMzcWGmY9RntlESghcBh8fB5xFw9ZFwMpPjkpEXAFcVkZUD1wLm1hcChGWy4uLmUZWgwTYi5iZ0NvbG9yPWk9RD1UPTA7Cz4yKiooH2xvZzIoCyl8MBppVjIpUC5zcGxpY2UoaSsxLFVbKFA6D08PKUosKFA6A08DKUoZXwwTaRxQKXtfOj1bVTBdE2scUClfOg9WKBAPFAEtEAMVASksXzoDVigQDxUBKxADFAEafVA9XztpPwspHUk9CyxHDDtJLS07KUcFSSsLSi0LLB4CMF0sAjFdKS8LLB9hdGFuMigCMV0sAjBdKRlHLnNvcnQoKGksQik9PkIDLWkDGlE9Cy0xfTs/GnNldEludGVydmFsKEZ7VFYuMDA3O2pePUM9SD1nPTA7enRhFmpKLEwafiNmZmZgEiBwKXpiYRYpLE5pDyxpAywxLFU3KSwHKQhOGyxpAyxVNxp6bRYbGkNWaQMUDkhWaQMVDnpsFhsaZzw9UREHGgQ9BHx8W107BBg8OTk5EQQFGxlnKyt9Zz0wCHpsZF09NSx+aHNsKCR7KFEtZykqOX0sNTAlLDUwJWAsem0WBA8sBAMpE0sgb2YgBCkoZz09H21pbihRLAstMSl8fEQRZxFnPD1REWclODwxKRF6bBZLDyxLAxoHGmcrK319LDkpJztHPS9bAS0fVVZOT0VGfnpxallMSj86NF0vLmV4ZWMoXyk7KXdpdGgoXy5zcGxpdChHKSlfPWpvaW4oc2hpZnQoKSk7ZXZhbChfKQ==
===========
Epic Cycles
===========
// The user can:
// - click (or click and move) to add points.
// - press up and down arrows to change the number of gears.
// - keep pressing up to change the color of the drawn path.
// - press left to show or hide the lines drawn by intermediate gears.
// - press right to clear the scene and start a new epicycloid.
// m : mouse down flag.
m = 0;
// On mouse down: set m.
onmousedown = e => m = 1;
// On mouse move: if the mouse is down, recompute the epicycloid with the current mouse coordinates.
onmousemove = e => m && f(e);
// On mouse up: recompute the epicycloid with the current mouse coordinates and unset m.
onmouseup = e => (m = 0, f(e));
// When an arrow key is pressed, one of these four functions is called:
onkeydown = e => [
// Left arrow (keyCode 37): toggle D (the multiple path flag).
e => D ^= 1,
// Up arrow (keyCode 38): increase Q (the max number of gears to draw).
e => Q++,
// Right arrow (keyCode 39): clear p (the points coordinates), G (the gears), and recompute the epicycloid (to reset everything else).
e => (p = [], G = [], f()),
// Down arrow (keyCode 40): decrease Q.
e => Q--
// Execute the function stored at the index e.which - 37 (same as e.keyCode - 37).
][e.which - 37]();
// p is a 2D array representing the X and Y coordinates of each point.
p = [];
// By default (on load), a pattern representing JS1K is drawn.
// This pattern is made of 33 points:
/*
p = [
[0,0], [1,0], [1,3], [0,3], [0,2], [0,3], [1,3], [1,0], // J
[4,0], [2,0], [2,1], [4,2], [4,3], [2,3], // S
[5,3], [5,0], [4,1], [5,0], [5,3], [6,3], // 1
[6,0], [6,2], [7,1], [6,2], [7,3], [6,2], [6,3], // K
[4,3], [4,2], [2,1], [2,0], [1,0], [1,0] // Return at the beginning
]
*/
// Each pair of coordinates was encoded in the bits of an ASCII char in the form "0b10xxxyy" ("10" + 3 bits for X + 2 bits for Y).
// The "10" prefix is used to produce chars that are high enough in the ASCII range (64 to 96) because chars 0 to 32 are required by RegPack to compress the demo.
// The code below decodes X and Y from each character of the string "@DGCBCGDPHIRSKWTQTW[XZ]Z_Z[SRIHHH" and adds a multiplier and an offset to have a correct sizing and centering.
for(i of`@DGCBCGDPHIRSKWTQTW[XZ]Z_Z[SRIHDD`)
p.push([50 * (i.charCodeAt() >> 2) - 950, 50 * (i.charCodeAt() & 3) - 90]);
// f()
// This function adds a point in p (is the e parameter is set) and recomputes all the gears' sizes and positions.
f = e => {
// if e is set,
e
&&
// and if p is empty, or if the distance between the mouse coordinates (e.x, e.y) and the last item of p is greater than 9px:
// (this is done to avoid drawing points too close to each other when we click and move the mouse slowly.)
(!p.length || Math.hypot(
e.x - a.width / 2 - p[p.length - 1][0],
e.y - a.height / 2 - p[p.length - 1][1]
) > 9)
&&
// Add the mouse coordinates to p.
// The coordinates are adjusted to take into account that [0;0] is at the center of the canvas.
p.push([e.x - a.width / 2, e.y - a.height / 2]);
// p is deep-copied into P.
// p is kept unchanged to draw the points on the canvas at each frame and to add new points with the mouse,
// while P will be transformed into a list of Fourier coefficients.
P = p.map(e => [...e]);
// Reset Z (the list of points that will be drawn by each gear of the epicycloid).
Z = [];
// Set the body's bgColor to 0 (this gives a black background to the page).
// Also, reset i (the loop var), D (the multiple line flag) and T (the time counter).
// This loop here fills each gap between two points of P with a new point, placed at the center of the two surrounding points.
// It stops when P has a length that is equal to a power of two.
// This "power of two" size would be mandatory if we computed a FFT of all the points,
// but to save bytes in this demo, we only compute a standard DFT. So this padding is finally only used only to add a nicer precision.
// For example the built-in "JS1K" logo contains 33 points, in order to generate 64 gears.
// It could have used less points, but it would look pretty bad with only 32 circles. You can see how bad by pressing "down" 32 times.
for (b.bgColor = i = D = T = 0; P.length > 2 ** (Math.log2(P.length) | 0); i += 2)
P.splice(i + 1, 0, [(P[i][0] + P[i + 1][0]) / 2, (P[i][1] + P[i + 1][1]) / 2]);
// Compute the DFT of P.
// This is black magic inspired by Paul Bourke's C++ code.
_ = [];
for (i in P) {
_[i] = [0, 0];
for (k in P)
// Perform complex rotations or something like that?
_[i][0] += (P[k][0] * Math.cos(k * -2 * Math.PI * i / P.length) - P[k][1] * Math.sin(k * -2 * Math.PI * i / P.length)),
_[i][1] += (P[k][0] * Math.sin(k * -2 * Math.PI * i / P.length) + P[k][1] * Math.cos(k * -2 * Math.PI * i / P.length));
}
// Anyway, now we have the Fourier transform of the points computed and stored in P.
P = _;
// If P is not empty:
if(P.length)
// Compute the sizes and positions of the gears and place the values in G based on the DFT we just computed, converted to polar coordinates.
for (I = P.length, G = []; I--;)
G.push(
[
// Angle offset of the gear (based on the position of the current gear).
I + P.length / 2 - P.length,
// Radius of the gear (based on the magnitude of the current Fourier coefficient).
Math.hypot(P[(I + P.length / 2) % P.length][0], P[(I + P.length / 2) % P.length][1]) / P.length,
// Frequency of the gear (based of the frequency of the current Fourier coefficient).
Math.atan2(P[(I + P.length / 2) % P.length][1], P[(I + P.length / 2) % P.length][0])
]
);
// Sort all the gears from the biggest to the smallest.
// This is not mandatory (the gears can be in any order), but it looks better if we do it.
G.sort((i, B) => B[1] - i[1]);
// Set Q to the position of the last value of P to draw a path as precise as possible by default.
Q = P.length - 1
};
// Call f() on load to compute the gear of the "JS1K" pattern.
f();
// Run the drawing loop (every 9 ms):
setInterval(
e => {
// Increase the time counter (T) very slowly, to allow one complete cycle to take about 1000 frames.
T += .007;
// Reset the canvas (a) and the vars U, V and g (center of the current gear and gear counter).
a.width ^= U = V = g = 0;
// Place the origin of the canvas at the center of the screen.
c.translate(a.width / 2, a.height / 2);
// Set the line color in white.
c.strokeStyle = `#fff`;
// Draw each point of p as a tiny circle.
for (i of p)
c.beginPath(),
c.arc(i[0], i[1], 1, 0, 7),
c.stroke();
// Draw each gear as a circle, and draw a line between the center of each gear (U, V) and the center of the next gear (new values of U, V).
for (i of G){
c.beginPath();
c.arc(U, V, i[1], 0, 7);
c.moveTo(U, V);
U += i[1] * Math.cos(i[0] * T + i[2]);
V += i[1] * Math.sin(i[0] * T + i[2]);
c.lineTo(U, V);
// The g'th gear is actually traced in white, only if g is lower than the limit defined in Q.
g <= Q && c.stroke();
// Prepare the array of points traced by the g'th gear (Z[g]) if it has been erased by f() earlier.
Z[g] = Z[g] || [];
// Add U and V to Z[g], only if less than 999 points have been stored already.
// (after ~999 points, the epicycloid loops and new points overlap the old ones, so we avoid storing / drawing them to increase the CPU performances).
Z[g].length < 999 && Z[g].push([U,V]);
g++
}
// Draw the path of each gear (if it is allowed by the values of Q and D).
// g is reused as a gear counter.
g = 0;
for (i of G){
c.beginPath();
// The lines have a width of 5px...
c.lineWidth = 5,
// ... and a HSL color with a hue equal to "(Q - g) * 9".
// The closing parenthesis of hsl() is not mandatory.
c.strokeStyle = `hsl(${(Q-g)*9},50%,50%`,
c.moveTo(Z[g][0], Z[g][1]);
for(j of Z[g])
// Only draw the g'th path if g is equal to the last gear of G, or equal to Q (if Q has a lower value than the last gear).
// Also, if D is set, draw intermediate lines every 8 gears (gear 0 not included).
(g == Math.min(Q, P.length - 1) || D && g && g <= Q && g % 8 < 1) && c.lineTo(j[0], j[1]);
c.stroke();
g++
}
},
9)