perfect-cursors

Perfect interpolation for animated multiplayer cursors. Used in tldraw.

Usage no npm install needed!

<script type="module">
  import perfectCursors from 'https://cdn.skypack.dev/perfect-cursors';
</script>

README

perfect-cursors

Perfect interpolation for animated multiplayer cursors. Used in tldraw.

💕 Love this library? Consider becoming a sponsor.

Installation

yarn add perfect-cursors
# or
npm i perfect-cursors

Usage

Quick n' dirty docs.

This library may be used to smoothly animate a cursor based on presence information from a multiplayer backend, such as the Presence layer of Y.js.

Usage in React

To use the library in React, create a React hook called usePerfectCursor.

// hooks/usePerfectCursor

export function usePerfectCursor(
  cb: (point: number[]) => void,
  point?: number[]
) {
  const [pc] = React.useState(() => new PerfectCursor(cb))

  React.useLayoutEffect(() => {
    if (point) pc.addPoint(point)
    return () => pc.dispose()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pc])

  const onPointChange = React.useCallback(
    (point: number[]) => pc.addPoint(point),
    [pc]
  )

  return onPointChange
}

And then a Cursor component that looks something like this:

// components/Cursor

import * as React from "react"
import { usePerfectCursor } from "../hooks/usePerfectCursors"

export function Cursor({ point }: { point: number[] }) {
  const rCursor = React.useRef<SVGSVGElement>(null)

  const animateCursor = React.useCallback((point: number[]) => {
    const elm = rCursor.current
    if (!elm) return
    elm.style.setProperty(
      "transform",
      `translate(${point[0]}px, ${point[1]}px)`
    )
  }, [])

  const onPointMove = usePerfectCursor(animateCursor)

  React.useLayoutEffect(() => onPointMove(point), [onPointMove, point])

  return (
    <svg
      ref={rCursor}
      style={{
        position: "absolute",
        top: -15,
        left: -15,
        width: 35,
        height: 35,
      }}
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 35 35"
      fill="none"
      fillRule="evenodd"
    >
      <g fill="rgba(0,0,0,.2)" transform="translate(1,1)">
        <path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
        <path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
      </g>
      <g fill="white">
        <path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
        <path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
      </g>
      <g fill={"red"}>
        <path d="m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" />
        <path d="m13 10.814v11.188l2.969-2.866.428-.139h4.768z" />
      </g>
    </svg>
  )
}

When the user's cursor point changes, pass that information to the Cursor component.

Community

License

This project is licensed under MIT.

If you're using the library in a commercial product, please consider becoming a sponsor.

Author