A spinning circle on a spinning circle



Remember those plastic toys where you put a pencil in a small hole in a circle and spin it inside a larger circle to draw geometric shapes? Let's build something similar in JavaScript!

Such shapes are called hypotrochoids in geometry, but who cares. Let's begin our coding exercise with a simple version:

<html>
<body>
<canvas id="myCanvaswidth="800height="600"></canvas>
<script>
 
function animate() {
  let tau = counter / (Math.PI * 15.5);
  let tau2 = counter / Math.PI;
  let x1 = Math.sin(tau) * r1 + 200;
  let y1 = Math.cos(tau) * r1 + 200;
  let x2 = x1 + Math.sin(tau2) * r2;
  let y2 = y1 + Math.cos(tau2) * r2;
  context.fillRect(x2, y2, 22);
  counter++;
  window.requestAnimationFrame(animate);
  }
 
let canvas = document.getElementById('myCanvas');
let context = canvas.getContext('2d');
let counter = 0;
let r1 = 100;
let r2 = 15;
animate();
 
</script>
</body>
</html>


Here's how it works:

[1-4] HTML setup
[6-16] the main animation loop:
[7-8] calculate the angles of the small and the big circle. We'll make big one spin 15.5 times slower than the small one.
[9-10] calculate the point on the big circle - this will be the center of the small circle
[11-12] calculate the point on the small circle - this point will actually be drawn on the canvas
[13] draw the point on the small circle
[14] increase the counter
[15] request the next animation frame

Now we just need to kick it off:
[18-19] canvas setup
[20] initialize the counter
[21-22] arbitrary values for the small (r1) and big (r2) circle radiuses
[23] start the animation!

Now that you get the idea, let's make it easier to change the radiuses and the angular velocities of the circles.
Here's what it will look like. On mobile devices it looks best in a new window (click here).



This version automatically resizes the canvas [6-23] to accomodate different screen sizes.
If the screen is vertical (eg. on mobile phones), the size of the picture (width and height) is set to equal the screen width.
If the screen is horizontal ([8]), the picture size is set to 60% of the screen height, allowing 40% for the menu.[11]
We added four sliders it the 'menu' section at the bottom of the canvas that let you control the circle parameters.
The menu is a bitmap drawn in every frame [32].
The four controls are just vertical rectangles drawn on top of the bitmap. [33-36]
The handling of the clicks is done in [69-86]. The aspectX variable is introduced to accomodate different resolutions, so that the sliders span horizonatlly across the entire picture width.
Here's the full code:

<html>
<body>
<canvas id="myCanvaswidth="600height="850"></canvas>
<script>
 
function resize() {
  let screenAspect = window.innerWidth / window.innerHeight;
  if (screenAspect < .8) {
    picSize = window.innerWidth;
  } else {
    picSize = window.innerHeight * .6;
  }
  aspectX = picSize / 500;
  canvas.width = picSize;
  canvas.height = window.innerHeight;
  menuItemHeight = (canvas.height - picSize) / 5;
}
 
function rectangle(xywidthheightcolor) {
  context.fillStyle = color;
  context.fillRect(xywidthheight);
  context.fillStyle = 'white';
}
 
function animate() {
  let angle1 = counter / (Math.PI * velocity1);
  let angle2 = counter / (Math.PI * velocity2);
  let x1 = Math.sin(angle1) * r1 + picSize / 2;
  let y1 = Math.cos(angle1) * r1 + picSize / 2;
  let x2 = x1 + Math.sin(angle2) * r2;
  let y2 = y1 + Math.cos(angle2) * r2;
  context.drawImage(image004005000picSizepicSizemenuItemHeight * 5);
  rectangle(r1 * factor1 * aspectXpicSize5menuItemHeight, 'black');
  rectangle(r2 * factor2 * aspectXpicSize + menuItemHeight5menuItemHeight, 'black');
  rectangle(velocity1 * factor3 * aspectXpicSize + menuItemHeight * 25menuItemHeight, 'black');
  rectangle(velocity2 * factor4 * aspectXpicSize + menuItemHeight * 35menuItemHeight, 'black');
  rectangle(x2, y2, 22, 'black');
  if (play)
    counter++;
  window.requestAnimationFrame(animate);
}
 
let canvas = document.getElementById('myCanvas');
canvas.style.position = "absolute";
canvas.style.left = "0";
canvas.style.top = "0";
let context = canvas.getContext('2d');
let picSizemenuItemHeightaspectX;
let counter = 0;
let r1 = 200;
let r2 = 20;
let velocity1 = 10;
let velocity2 = 1.5;
let factor1 = 2;
let factor2 = 10;
let factor3 = 10;
let factor4 = 40;
let play = true;
 
let image = new Image();
image.src = "menu.png";
image.onload = function() {
  resize();
  animate();
};
 
window.onresize = resize;
 
canvas.onclick = function(event) {
  context.drawImage(image002001000picSizepicSizemenuItemHeight * 5);
  let x = event.offsetX / aspectX;
  let y = event.offsetY;
  if (y <= picSize) {
    rectangle(00picSizepicSize, 'white');
  }
  if (y > picSize && y <= picSize + menuItemHeight)
    r1 = x / factor1;
  if (y > picSize + menuItemHeight && y <= picSize + menuItemHeight * 2)
    r2 = x / factor2;
  if (y > picSize + menuItemHeight * 2 && y <= picSize + menuItemHeight * 3)
    velocity1 = x / factor3;
  if (y > picSize + menuItemHeight * 3 && y < picSize + menuItemHeight * 4)
    velocity2 = x / factor4;
  if (y > picSize + menuItemHeight * 4)
    play = !play;
};
</script>
</body>
</html>


Check out these programming tutorials:

JavaScript:

Particle constellations (42 lines)

3d stereogram (0 lines)

Optical illusion (18 lines)

Spinning squares - visual effect (25 lines)

Oldschool fire effect (20 lines)

Fireworks (60 lines)

Animated fractal (32 lines)

Minesweeper game (80 lines)

Physics engine for beginners

Physics engine - interactive sandbox

Physics engine - silly contraption

Starfield (21 lines)

Yin Yang with a twist (4 circles and 20 lines)

Tile map editor (70 lines)

Sine scroller (30 lines)

Turtle graphics

Interactive animated sprites

Image transition effect (16 lines)

Wholla lotta quadratic curves (50 lines)

Your first program in JavaScript: you need 5 minutes and a notepad


Fractals in Excel

Python in Blender 3d:

Domino effect (10 lines)


Wrecking ball effect (14 lines)

3d fractal in Blender Python