Catch It began as my first experiment with the then-new HTML5 <canvas> element. At the time, drawing and animating directly in a browser without Flash or a plugin was still unusual. Google later featured the game as a Chrome Experiment, which made this small weekend project an important milestone in my early browser graphics work.
The game itself is deliberately simple: move the red cursor onto the red target while avoiding the dark balls. Every catch adds another moving obstacle. Clicking or tapping repels nearby balls, but also accelerates them. The implementation in the Stage keeps that original idea while removing an experimental power-up that temporarily made obstacles transparent; visibility should not be part of the collision difficulty.
Motion and Wall Reflection
Each obstacle has a position \(\mathbf p=(x,y)\) and velocity \(\mathbf v=(v_x,v_y)\). For a frame interval \(\Delta t\), explicit integration advances it by
\[ \mathbf p_{n+1}=\mathbf p_n+\mathbf v_n\Delta t. \]If a ball of radius \(r\) reaches a vertical wall, its horizontal velocity changes sign; a horizontal wall changes the vertical component:
\[ (v_x,v_y)\mapsto(-v_x,v_y) \quad\text{or}\quad (v_x,v_y)\mapsto(v_x,-v_y). \]The position is clamped back inside the board after reflection. This matters when a slow frame advances the ball beyond the boundary: reversing velocity alone would leave it outside and could trigger another reversal on the next frame.
Fast Circle Collision Detection
Let the player and an obstacle have centers \(\mathbf m\) and \(\mathbf b\), with radii \(R\) and \(r\). They overlap exactly when the distance between their centers does not exceed the sum of their radii:
\[ \lVert\mathbf m-\mathbf b\rVert\le R+r. \]Squaring both sides removes the square root without changing the comparison because both sides are nonnegative:
\[ (m_x-b_x)^2+(m_y-b_y)^2\le(R+r)^2. \]A cheap broad-phase test rejects most balls first. If either \(|m_x-b_x|>R+r\) or \(|m_y-b_y|>R+r\), the circles cannot intersect. Only candidates inside that bounding square need the exact squared-distance calculation. With a player radius of 10 and obstacle radius of 5, the final threshold is \((10+5)^2=225\).
const reach = player.radius + ball.radius;
const dx = player.x - ball.x;
const dy = player.y - ball.y;
if (Math.abs(dx) <= reach &&
Math.abs(dy) <= reach &&
dx * dx + dy * dy <= reach * reach) {
loseRound();
} Circle Against the Target Rectangle
The target is a square rather than a circle. The closest point on an axis-aligned rectangle is found by clamping the player center independently to the rectangle's horizontal and vertical intervals. If that closest point is within the player radius, the target has been caught:
\[ q_x=\operatorname{clamp}(m_x,x_0,x_1),\qquad q_y=\operatorname{clamp}(m_y,y_0,y_1), \] \[ (m_x-q_x)^2+(m_y-q_y)^2\le R^2. \]Click Repulsion
A click at \(\mathbf c\) applies an outward impulse to each obstacle. Using the displacement \(\mathbf d=\mathbf p-\mathbf c\), the game adds
\[ \Delta\mathbf v=k\frac{\mathbf d}{\max(\lVert\mathbf d\rVert^2,\varepsilon)}. \]The small \(\varepsilon\) prevents division by zero when clicking directly on a center, and a speed cap keeps repeated clicks from producing numerically unstable motion. The result preserves the original trade-off: repulsion may clear an escape route, but every use makes the remaining round harder.
Today, this is elementary browser graphics. In the early Canvas era, however, it was a practical demonstration that real-time drawing, input, collision detection, and game state could live in an ordinary web page. That historical context is what makes the experiment worth preserving.