menu
search
...

add-a-cat-following-mouse

eye-

Background

I found a lovely cat on Internet, it's Github repo: https://github.com/adryd325/oneko.js. It's written in javascript. I create a react version.

Implement

First, let's add a cat on screen. You can use a sprite image. You can download it here:

"use client"

export default function MyCat() {
  return (
    <div
      className="fixed pointer-events-none z-50"
      style={{
        width: `32px`,
        height: `32px`,
        backgroundImage: "url(/cat.gif)",
      }}
    />
  )
}

We get a cat on screen now! Let's change it's position. We want to show it on top left corner.

  <div
      className="fixed pointer-events-none z-50"
      style={{
        width: `32px`,
        height: `32px`,
        backgroundImage: "url(/cat.gif)",
      + backgroundPosition: "-96px -96px",
      + left: "16px",
      + top: "16px",
      + zIndex: 2147483647 // 2 ** 31 - 1
      }}
    />

We need to monitor mouse movement, so that cat can reach there.

const mousePosXRef = useRef(0) const mousePosYRef = useRef(0)

const handleMouseMove = (event: MouseEvent) => {
  mousePosXRef.current = event.clientX
  mousePosYRef.current = event.clientY
}

useEffect(() => {
  window.addEventListener("mousemove", handleMouseMove)

  return () => {
    window.removeEventListener("mousemove", handleMouseMove)
  }
}, [])

Now let's image, we have a point: (x, y), we need to let cat reach there, how to implement it? Yes, we need to animation.

function frame() {
  // cat run
}

function onAnimationFrame(timestamp) {
  frame()
  requestAnimationFrame(onAnimationFrame)
}

requestAnimationFrame(onAnimationFrame)

We need to bring the cat closer to the mouse position during each animation.

const catPostXRef = useRef(32)
const catPostYRef = useRef(32)

const diffX = mousePosXRef.current - catPostXRef.current
const diffY = mousePosYRef.current - catPostYRef.current

const distance = Math.sqrt(diffX ** 2 + diffY ** 2)

catPostXRef.current += (diffX / distance)
catPostYRef.current += (diffY / distance)

catPosXRef.current = Math.min(
  Math.max(16, catPosXRef.current), // at least 16
  window.innerWidth - 16 // make sure not large than window width - 16
)
catPosYRef.current = Math.min(
  Math.max(16, catPosYRef.current),
  window.innerHeight - 16
)

if (containerRef.current) {
  // cat center(32px / 2 = 16px)
  containerRef.current.style.left = `${catPosXRef.current - 16}px`
  containerRef.current.style.top = `${catPosYRef.current - 16}px`
}

Now the cat can follow our mouse. We need to add a conditional statement to stop animation:

if (distance < 48) {
  return
}

Also, we want to let cat move in segments, so we need an extra variable: CAT_SPEED.

const CAT_SPEED = 10
const FRAME_DELAY = 100

...

catPosXRef.current += (diffX / distance) * CAT_SPEED
catPosYRef.current += (diffY / distance) * CAT_SPEED
...

function onAnimationFrame(timestamp) {
  if (!lastFrameTimestampRef.current) {
    lastFrameTimestampRef.current = timestamp
  }

  if (timestamp - lastFrameTimestampRef.current > FRAME_DELAY) {
    lastFrameTimestampRef.current = timestamp
    frame()
  }

  requestAnimationFrame(onAnimationFrame)
}

Let's say we have eight directions.

{
  N: "North",
  S: "South",
  W: "West",
  E: "East",
  NE: "North-East",
  SE: "South-East",
  SW: "South-West",
  NW: "North-West"
}
abbreviationColor Management with Single Source of Truth