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.nameon connect - Add click effects with broadcast events
- Add cursor trails or particle effects