Cursor Party

Everyone broadcasts their position. Everyone renders everyone else.

When to Use

  • Collaborative tools (Figma-style cursors)
  • .io games (agar.io, slither.io)
  • Social experiences
  • Shared whiteboards
  • Any "see everyone moving" experience

The Pattern

import { connect } from '@watchtower-sdk/core'

const room = await connect('cursors')
const cursors = {}

// 1. Broadcast my position
document.onmousemove = (e) => {
  room.broadcast({ 
    x: e.clientX, 
    y: e.clientY,
    color: myColor,
    name: myName
  })
}

// 2. Receive others' positions
room.on('message', (from, data) => {
  cursors[from] = data
})

// 3. Clean up when players leave
room.on('leave', (player) => {
  delete cursors[player.id]
})

// 4. Render loop
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  for (const [id, cursor] of Object.entries(cursors)) {
    ctx.fillStyle = cursor.color
    ctx.beginPath()
    ctx.arc(cursor.x, cursor.y, 8, 0, Math.PI * 2)
    ctx.fill()
    
    ctx.fillStyle = '#fff'
    ctx.fillText(cursor.name, cursor.x + 12, cursor.y + 4)
  }
  
  requestAnimationFrame(draw)
}
draw()

Key Points

  • No authority — everyone controls their own cursor
  • High update rate — send on every mouse move (or throttle to 20-60Hz)
  • Simple state — just x, y, and optional metadata
  • Clean up on leave — remove cursors when players disconnect

Throttling Updates

Sending on every mousemove can be excessive. Throttle to 20-50Hz:

let lastSend = 0
const SEND_RATE = 50 // ms between sends

document.onmousemove = (e) => {
  const now = Date.now()
  if (now - lastSend < SEND_RATE) return
  lastSend = now
  
  room.broadcast({ x: e.clientX, y: e.clientY })
}

Adding Smoothing

For smoother cursor movement, lerp toward target positions:

const LERP = 0.3 // Higher = snappier

room.on('message', (from, data) => {
  if (!cursors[from]) {
    cursors[from] = { x: data.x, y: data.y, targetX: data.x, targetY: data.y }
  } else {
    cursors[from].targetX = data.x
    cursors[from].targetY = data.y
  }
})

function update() {
  for (const cursor of Object.values(cursors)) {
    cursor.x += (cursor.targetX - cursor.x) * LERP
    cursor.y += (cursor.targetY - cursor.y) * LERP
  }
  requestAnimationFrame(update)
}
update()

Full Example with HTML

<!DOCTYPE html>
<html>
<head>
  <title>Cursor Party</title>
  <style>
    body { margin: 0; overflow: hidden; background: #111; }
    canvas { display: block; }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script type="module">
    import { connect } from 'https://unpkg.com/@watchtower-sdk/core'
    
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    canvas.width = window.innerWidth
    canvas.height = window.innerHeight
    
    const room = await connect()
    alert('Share this code: ' + room.code)
    
    const cursors = {}
    const myColor = '#' + Math.floor(Math.random()*16777215).toString(16)
    
    document.onmousemove = (e) => {
      room.broadcast({ x: e.clientX, y: e.clientY, color: myColor })
    }
    
    room.on('message', (from, data) => {
      cursors[from] = data
    })
    
    room.on('leave', (player) => {
      delete cursors[player.id]
    })
    
    function draw() {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      
      for (const cursor of Object.values(cursors)) {
        ctx.fillStyle = cursor.color
        ctx.beginPath()
        ctx.arc(cursor.x, cursor.y, 10, 0, Math.PI * 2)
        ctx.fill()
      }
      
      requestAnimationFrame(draw)
    }
    draw()
  </script>
</body>
</html>

Next Steps

  • Add player names with options.name on connect
  • Add click effects with broadcast events
  • Add cursor trails or particle effects