const ROTATE = Math.PI / 50;
const WHEEL_SIZE = 0.6;
Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randPow = (min, max, p) => Math.random() ** p * (max - min) + min;
var friction = 0.35;
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);
ctx.font = "30px arial";
ctx.textAlign = "center";
scrollBy(0, canvas.height / 2 - canvas.height / 2 * WHEEL_SIZE);
function mainLoop() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
wheel.update();
ball.update(wheel, arrow);
wheel.draw();
path.draw();
ball.draw();
arrow.draw(ball);
requestAnimationFrame(mainLoop);
}
const path = Object.assign([],{
draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.strokeStyle = "#F00";
ctx.lineWidth = 1;
ctx.beginPath();
for (const p of this) { ctx.lineTo(p.x, p.y) }
ctx.stroke();
},
reset() { this.length = 0 },
add(point) {
this.push({x: point.x, y: point.y});
if (this.length > 1000) { // prevent long lines from slowing render
this.shift()
}
}
});
const arrow = {
dx: 0,dy: 0,
draw(ball) {
if (this.dx || this.dy) {
const dir = Math.atan2(this.dy, this.dx);
// len is converted from frame 1/60th second to seconds
const len = Math.hypot(this.dy, this.dx) * 60;
const aXx = Math.cos(dir);
const aXy = Math.sin(dir);
ctx.setTransform(aXx, aXy, -aXy, aXx, ball.x, ball.y);
ctx.beginPath();
ctx.lineTo(0,0);
ctx.lineTo(len, 0);
ctx.moveTo(len - 4, -2);
ctx.lineTo(len, 0);
ctx.lineTo(len - 4, 2);
ctx.strokeStyle = "#FFF";
ctx.lineWidth = 2;
ctx.stroke();
}
}
};
const ball = {
x: canvas.width / 2 + 4,
y: canvas.height / 2,
dx: 0, // delta pos Movement vector
dy: 0,
update(wheel, arrow) {
// get distance from center
const dist = Math.hypot(wheel.x - this.x, wheel.y - this.y);
// zero force arrow
arrow.dx = 0;
arrow.dy = 0;
// check if on wheel
if (dist < wheel.radius) {
// get tangent vector direction
const tangent = Math.atan2(this.y - wheel.y, this.x - wheel.x) + Math.PI * 0.5 * Math.sign(wheel.dr);
// get tangent as vector
// which is distance times wheel rotation in radians.
const tx = Math.cos(tangent) * dist * wheel.dr;
const ty = Math.sin(tangent) * dist * wheel.dr;
// get difference between ball vector and tangent vector scaling by friction
const fx = arrow.dx = (tx - this.dx) * friction;
const fy = arrow.dy = (ty - this.dy) * friction;
// Add the force vector
this.dx += fx;
this.dy += fy;
} else if (dist > wheel.radius * 1.7) { // reset ball
// to ensure ball is off center use random polar coord
const dir = Math.rand(0, Math.PI * 2);
const dist = Math.randPow(1, 20, 2); // add bias to be close to center
this.x = canvas.width / 2 + Math.cos(dir) * dist;
this.y = canvas.height / 2 + Math.sin(dir) * dist;
this.dx = 0;
this.dy = 0;
path.reset();
}
// move the ball
this.x += this.dx;
this.y += this.dy;
path.add(ball);
},
draw() {
ctx.fillStyle = "#0004";
ctx.setTransform(1, 0, 0, 1, this.x + 5, this.y + 5);
ctx.beginPath();
ctx.arc(0, 0, 10, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = "#f00";
ctx.setTransform(1, 0, 0, 1, this.x, this.y);
ctx.beginPath();
ctx.arc(0, 0, 12, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = "#FFF8";
ctx.setTransform(1, 0, 0, 1, this.x - 5, this.y - 5);
ctx.beginPath();
ctx.ellipse(0, 0, 2, 3, -Math.PI * 0.75, 0, 2 * Math.PI);
ctx.fill();
},
}
const wheel = {
x: canvas.width / 2, y: canvas.height / 2, r: 0,
dr: ROTATE, // delta rotate
radius: Math.min(canvas.height, canvas.width) / 2 * WHEEL_SIZE,
text: "wheel",
update() { this.r += this.dr },
draw() {
const aXx = Math.cos(this.r);
const aXy = Math.sin(this.r);
ctx.setTransform(aXx, aXy, -aXy, aXx, this.x, this.y);
ctx.fillStyle = "#CCC";
ctx.strokeStyle = "#000";
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
ctx.strokeStyle = ctx.fillStyle = "#aaa";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.lineTo(-20,0);
ctx.lineTo(20,0);
ctx.moveTo(0,-20);
ctx.lineTo(0,20);
ctx.stroke();
ctx.fillText(this.text, 0, this.radius - 16);
},
}
<canvas id="canvas" width="300" height="300"></canvas>