This demonstrates:
- Cursor as a procedural shape, not an icon
- Smooth morphing based on context
- Zero text, zero UI chrome
- Clean primitive you can reuse everywhere
Behaviors implemented
- Idle → small dot
- Hover object → square
- Attack zone → cross
- Dock zone → ring
- Smooth interpolation between shapes
- World-space logic (not DOM hover)
Code for Above
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Cursor Morph POC</title>
<style>
html, body {
margin: 0;
background: #000;
overflow: hidden;
}
canvas {
display: block;
margin: 0 auto;
background: radial-gradient(#0a0a0a, #000);
cursor: none;
}
</style>
</head>
<body>
<canvas id="c" width="900" height="900"></canvas>
<script>
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const mouse = { x: 450, y: 450 };
let targetMode = "idle";
let mode = "idle";
let t = 0;
const zones = [
{ x: 250, y: 250, r: 60, mode: "dock" },
{ x: 650, y: 250, r: 60, mode: "attack" },
{ x: 450, y: 600, r: 70, mode: "hover" }
];
canvas.addEventListener("mousemove", e => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
function dist(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function updateMode() {
targetMode = "idle";
for (const z of zones) {
if (dist(mouse, z) < z.r) {
targetMode = z.mode;
break;
}
}
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function drawCursor(alpha) {
ctx.save();
ctx.translate(mouse.x, mouse.y);
ctx.strokeStyle = `rgba(255,255,255,${alpha})`;
ctx.lineWidth = 2;
if (mode === "idle") {
ctx.beginPath();
ctx.arc(0, 0, lerp(3, 8, alpha), 0, Math.PI * 2);
ctx.fillStyle = "#fff";
ctx.fill();
}
if (mode === "hover") {
ctx.strokeRect(-8, -8, 16, 16);
}
if (mode === "attack") {
ctx.beginPath();
ctx.moveTo(-10, 0);
ctx.lineTo(10, 0);
ctx.moveTo(0, -10);
ctx.lineTo(0, 10);
ctx.stroke();
}
if (mode === "dock") {
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
function drawZones() {
for (const z of zones) {
ctx.beginPath();
ctx.arc(z.x, z.y, z.r, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(255,255,255,0.15)";
ctx.stroke();
}
}
function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
updateMode();
if (mode !== targetMode) {
t += 0.08;
if (t >= 1) {
t = 0;
mode = targetMode;
}
} else {
t = 0;
}
drawZones();
drawCursor(1 - t);
if (t > 0) {
const prev = mode;
mode = targetMode;
drawCursor(t);
mode = prev;
}
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>