Back to blog
08 Jan 2025
5 min read

Robu the Robot: When Math, SolidJS, and Whimsy Collide

How Trigonometry and SolidJS Brought a Robot to Life

The Origin Story 🤖

After seeing some cool interactive logos around the web, I wanted to add a bit of personality to my portfolio. Enter Robu - a simple yet charming robot mascot that follows your cursor around and occasionally blinks at you.

The Evolution: A Tale of Five Robos

Robu Evolution

It all started with v0, your typical “Generate a robot logo” chatbot face – complete with antenna and that generic tech vibe. One night, fueled by coffee and a sudden urge to redesign everything (we’ve all been there), I started playing with circles. What if we just… stripped everything down? v1 emerged with its wide-eyed curiosity, but something still wasn’t quite right. After some color flipping and pixel pushing, each iteration got progressively more minimal. By v4, Robu had found its identity – a perfect balance of simplicity and character. Sometimes less really is more, especially when that “less” can track your cursor around the screen!

🛠️ Why SolidJS ?

Let’s be real – as a software engineer, having a sluggish portfolio is like showing up to a job interview in pajamas. I decided to give SolidJS a shot, part curiosity, part performance obsession, and maybe a tiny bit of that “I should try the new shiny thing” developer syndrome. Turns out, SolidJS’s reactive primitives were perfect for handling all those smooth cursor-following calculations.

What’s the Deal with Signals?

First time I saw SolidJS signals, I was like “Why are we calling functions to get values? This seems… weird.” But after actually using them for Robu’s animations, it clicked. Signals aren’t just state containers - they’re more like tiny reactivity managers:

// React's way:
// const [positions, setPositions] = useState({ ... })
// Virtual DOM diffing, scheduling updates, the whole orchestra

// SolidJS's approach - signals are functions!
const [positions, setPositions] = createSignal({
  head: {
    x: 50.4,
    y: 50.4,
  },
  leftEye: {
    x: 43.344,
    y: 52.08,
  },
  rightEye: {
    x: 60.344,
    y: 52.08,
  }
});

// Here's where it gets interesting - signals are getter functions
// This means positions() to get the value!
return (
  <circle
    id="head"
    r="35.28"
    cx={positions().head.x}
    cy={positions().head.y}
    class={cn("fill-zinc-100 dark:fill-zinc-900")}
  />
);

// createMemo - like useMemo, but without the “Oops, forgot to update the dependency array” drama 
// yes, this is a simplified example
const eyePosition = createMemo(() => {
  const pos = positions();
  return {
    left: { 
      x: pos.leftEye.x + (pos.head.x - 50.4) * 0.5, 
      y: pos.leftEye.y + (pos.head.y - 50.4) * 0.5 
    },
    right: { 
      x: pos.rightEye.x + (pos.head.x - 50.4) * 0.5, 
      y: pos.rightEye.y + (pos.head.y - 50.4) * 0.5 
    }
  };
});

For something tracking cursor movement and updating multiple elements 60 times a second, this fine-grained reactivity is exactly what you need. No virtual DOM diffing, no scheduling - just direct updates to what changed. Now those parentheses don’t look so weird anymore, do they?

Cursor Tracking (or, Thank God Someone Remembered Their Trigonometry)

Ever wondered how those smooth cursor-following animations work? Turns out there’s some clever high school math behind the scenes. Here’s the part that makes Robu’s eyes move so naturally:

const handleMove = (clientX: number, clientY: number) => {
  const svgElement = document.getElementById("robu");
  if (!svgElement) return;
  
  // Getting mouse position relative to Robu
  const rect = svgElement.getBoundingClientRect();
  const svgCenterX = rect.left + rect.width / 2;
  const svgCenterY = rect.top + rect.height / 2;
  const offsetX = clientX - svgCenterX;
  const offsetY = clientY - svgCenterY;

  // Here's where someone's math teacher is feeling vindicated
  const maxHeadOffset = 4;
  const headDistance = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
  const clampedHeadDistance = Math.min(headDistance, maxHeadOffset);
  const headAngle = Math.atan2(offsetY, offsetX);
  const headMoveX = clampedHeadDistance * Math.cos(headAngle);
  const headMoveY = clampedHeadDistance * Math.sin(headAngle);

  // More math magic for smooth eye tracking...
};

Who knew those sin, cos, and atan2 formulas would come in so handy? Somewhere out there, a math teacher is saying “I told you so.”

The Perfectionist’s Touch: Cursor Memory

You probably didn’t even notice this one (that’s the point!) - Robu’s eyes used to have a brief moment of existential crisis after each page navigation, snapping back to center like they’d forgotten where they were looking. Not anymore! After a slightly obsessive debugging session fueled by my inability to unsee it, here’s the elegant solution:

// Because Robu deserves object permanence
const trackMousePosition = (event: MouseEvent) => {
  window.mouseX = event.clientX;
  window.mouseY = event.clientY;
};

// No more "wait, what was I looking at?" moments
const initializePosition = () => {
  const lastKnownX = window.mouseX || window.innerWidth / 2;
  const lastKnownY = window.mouseY || window.innerHeight / 2;
  handleMove(lastKnownX, lastKnownY);
};

Now Robu maintains that perfect eye contact across page loads like the sophisticated robot it is.

What’s Next? 🚀

I’ve got a few ideas for future updates:

  1. Add some expressions based on scroll position
  2. Easter egg animations on click
  3. Maybe some subtle sounds (with user permission, of course!)

Robu is pretty simple, but it adds that little bit of whimsy that makes browsing more fun


Inspired by various interactive mascots around the web, specifically Mark Horn’s batcat implementation.