1.22.6 ā€¢ Published 8 months ago

@ivine/tldcore v1.22.6

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

@tldraw/core

This package contains the Renderer and core utilities used by tldraw.

You can use this package to build projects like tldraw, where React components are rendered on a canvas user interface. Check out the advanced example.

šŸ’• Love this library? Consider becoming a sponsor.

Installation

Use your package manager of choice to install @tldraw/core and its peer dependencies.

yarn add @tldraw/core && yarn build
# or
npm i @tldraw/core && npm run build

Note: You'll need to run the build script before running dev.

Examples

There are two examples in this repository.

The simple example in examples/core-example shows a minimal use of the library. It does not do much but this should be a good reference for the API without too much else built on top.

The advanced example in examples/core-example-advanced shows a more realistic use of the library. (Try it here). While the fundamental patterns are the same, this example contains features such as: panning, pinching, and zooming the camera; creating, cloning, resizing, and deleting shapes; keyboard shortcuts, brush-selection; shape-snapping; undo, redo; and more. Much of the code in the advanced example comes from the @tldraw/tldraw codebase.

If you're working on an app that uses this library, I recommend referring back to the advanced example for tips on how you might implement these features for your own project.

Usage

Import the Renderer React component and pass it the required props.

import * as React from "react"
import { Renderer, TLShape, TLShapeUtil, Vec } from '@tldraw/core'
import { BoxShape, BoxUtil } from "./shapes/box"

const shapeUtils = { box: new BoxUtil() }

function App() {
  const [page, setPage] = React.useState({
    id: "page"
    shapes: {
      "box1": {
        id: 'box1',
        type: 'box',
        parentId: 'page',
        childIndex: 0,
        point: [0, 0],
        size: [100, 100],
        rotation: 0,
      }
    },
    bindings: {}
  })

  const [pageState, setPageState] = React.useState({
    id: "page",
    selectedIds: [],
    camera: {
      point: [0,0],
      zoom: 1
    }
  })

  return (<Renderer
    page={page}
    pageState={pageState}
    shapeUtils={shapeUtils}
  />)
}

Documentation

Renderer

To avoid unnecessary renders, be sure to pass "stable" values as props to the Renderer. Either define these values outside of the parent component, or place them in React state, or memoize them with React.useMemo.

PropTypeDescription
pageTLPageThe current page object.
pageStateTLPageStateThe current page's state.
shapeUtilsTLShapeUtilsThe shape utilities used to render the shapes.
assetsTLAssets(optional) A table of assets used by shapes in the project.

In addition to these required props, the Renderer accents many other optional props.

PropertyTypeDescription
containerRefReact.MutableRefObjectA React ref for the container, where CSS variables will be added.
themeobjectAn object with overrides for the Renderer's default colors.
componentsobjectAn object with overrides for the Renderer's default React components.
hideBoundsbooleanDo not show the bounding box for selected shapes.
hideHandlesbooleanDo not show handles for shapes with handles.
hideBindingHandlesbooleanDo not show binding controls for selected shapes with bindings.
hideResizeHandlesbooleanDo not show resize handles for selected shapes.
hideRotateHandlesbooleanDo not show rotate handles for selected shapes.
hideCursorsbooleanDo not show multiplayer cursors.
snapLinesTLSnapLine[]An array of "snap" lines.
usersobjectA table of TLUsers.
userIdobjectThe current user's TLUser id.

The theme object accepts valid CSS colors for the following properties:

PropertyDescription
foregroundThe primary (usually "text") color
backgroundThe default page's background color
brushFillThe fill color of the brush selection box
brushStrokeThe stroke color of the brush selection box
selectFillThe fill color of the selection bounds
selectStrokeThe stroke color of the selection bounds and handles

The components object accepts React components for the following properties:

PropertyDescription
CursorMultiplayer cursors on the canvas

The Renderer also accepts many (optional) event callbacks.

PropDescription
onPanPanned with the mouse wheel
onZoomZoomed with the mouse wheel
onPinchStartBegan a two-pointer pinch
onPinchMoved their pointers during a pinch
onPinchEndStopped a two-pointer pinch
onPointerDownStarted pointing
onPointerMoveMoved their pointer
onPointerUpEnded a point
onPointCanvasPointed the canvas
onDoubleClickCanvasDouble-pointed the canvas
onRightPointCanvasRight-pointed the canvas
onDragCanvasDragged the canvas
onReleaseCanvasStopped pointing the canvas
onHoverShapeMoved their pointer onto a shape
onUnhoverShapeMoved their pointer off of a shape
onPointShapePointed a shape
onDoubleClickShapeDouble-pointed a shape
onRightPointShapeRight-pointed a shape
onDragShapeDragged a shape
onReleaseShapeStopped pointing a shape
onHoverHandleMoved their pointer onto a shape handle
onUnhoverHandleMoved their pointer off of a shape handle
onPointHandlePointed a shape handle
onDoubleClickHandleDouble-pointed a shape handle
onRightPointHandleRight-pointed a shape handle
onDragHandleDragged a shape handle
onReleaseHandleStopped pointing shape handle
onHoverBoundsMoved their pointer onto the selection bounds
onUnhoverBoundsMoved their pointer off of the selection bounds
onPointBoundsPointed the selection bounds
onDoubleClickBoundsDouble-pointed the selection bounds
onRightPointBoundsRight-pointed the selection bounds
onDragBoundsDragged the selection bounds
onReleaseBoundsStopped the selection bounds
onHoverBoundsHandleMoved their pointer onto a selection bounds handle
onUnhoverBoundsHandleMoved their pointer off of a selection bounds handle
onPointBoundsHandlePointed a selection bounds handle
onDoubleClickBoundsHandleDouble-pointed a selection bounds handle
onRightPointBoundsHandleRight-pointed a selection bounds handle
onDragBoundsHandleDragged a selection bounds handle
onReleaseBoundsHandleStopped a selection bounds handle
onShapeCloneClicked on a shape's clone handle
onShapeChangeA shape's component prompted a change
onShapeBlurA shape's component was prompted a blur
onRenderCountChangeThe number of rendered shapes changed
onBoundsChangeThe Renderer's screen bounding box of the component changed
onErrorThe Renderer encountered an error

The @tldraw/core library provides types for most of the event handlers:

Type
TLPinchEventHandler
TLPointerEventHandler
TLCanvasEventHandler
TLBoundsEventHandler
TLBoundsHandleEventHandler
TLShapeChangeHandler
TLShapeBlurHandler
TLShapeCloneHandler

TLPage

An object describing the current page. It contains:

PropertyTypeDescription
idstringA unique id for the page.
shapesTLShape{}A table of shapes.
bindingsTLBinding{}A table of bindings.
backgroundColorstring(optional) The page's background fill color. Will also overwrite the theme.

TLPageState

An object describing the current page. It contains:

PropertyTypeDescription
idstringThe corresponding page's id
selectedIdsstring[]An array of selected shape ids
cameraobjectAn object describing the camera state
camera.pointnumber[]The camera's [x, y] coordinates
camera.zoomnumberThe camera's zoom level
pointedIdstring(optional) The currently pointed shape id
hoveredIdstring(optional) The currently hovered shape id
editingIdstring(optional) The currently editing shape id
bindingIdstring(optional) The currently editing binding.
brushTLBounds(optional) A Bounds for the current selection box

TLAssets

An object describing the current page's assets. It contains:

PropertyTypeDescription
idstringA unique id for the asset.
typestringThe type of the asset.

Assets are used for shared resources, such as serialized images and videos in the @tldraw/tldraw app. If a shape has an assetId property, its component will receive the corresponding asset in the component's props. Like shapes, this interface is meant to be extended with additional properties relevant to the type of asset used (e.g. size, duration, url).

TLShape

An object that describes a shape on the page. The shapes in your document should extend this interface with other properties. See Shape Type.

PropertyTypeDescription
idstringThe shape's id.
typestringThe type of the shape, corresponding to the type of a TLShapeUtil
parentIdstringThe id of the shape's parent (either the current page or another shape)
childIndexnumberthe order of the shape among its parent's children
namestringthe name of the shape
pointnumber[]the shape's current [x, y] coordinates on the page
assetIdstring(optional) An asset id from the TLAssets table.
rotationnumber(optional) The shape's current rotation in radians
childrenstring[](optional) An array containing the ids of this shape's children
handles{}(optional) A table of TLHandle objects
isGhostboolean(optional) True if the shape is "ghosted", e.g. while deleting
isLockedboolean(optional) True if the shape is locked
isHiddenboolean(optional) True if the shape is hidden
isEditingboolean(optional) True if the shape is currently editing
isGeneratedbooleanoptional) True if the shape is generated programatically
isAspectRatioLockedboolean(optional) True if the shape's aspect ratio is locked

TLHandle

An object that describes a relationship between two shapes on the page.

PropertyTypeDescription
idstringAn id for the handle
indexnumberThe handle's order within the shape's handles
pointnumber[]The handle's [x, y] coordinates

When a shape with handles is the only selected shape, the Renderer will display its handles. You can respond to interactions with these handles using the `on

TLBinding

An object that describes a relationship between two shapes on the page.

PropertyTypeDescription
idstringA unique id for the binding
fromIdstringThe id of the shape where the binding begins
toIdstringThe id of the shape where the binding begins

TLSnapLine

A snapline is an array of points (formatted as [x, y]) that represent a "snapping" line.

TLShapeUtil

The TLShapeUtil is an abstract class that you can extend to create utilities for your custom shapes. See the Creating Shapes guide to learn more.

TLUser

A TLUser is the presence information for a multiplayer user. The user's pointer location and selections will be shown on the canvas. If the TLUser's id matches the Renderer's userId prop, then the user's cursor and selections will not be shown.

PropertyTypeDescription
idstringA unique id for the user
colorstringThe user's color, used for indicators
pointnumber[]The user's pointer location on the page
selectedIds[]string[]The user's selected shape ids

Utils

A general purpose utility class. See source for more.

Guide: Creating Shapes

The Renderer component has no built-in shapes. It's up to you to define every shape that you want to see on the canvas. While these shapes are highly reusable between projects, you'll need to define them using the API described below.

For several example shapes, see the folder /example/src/shapes/.

Shape Type

Your first task is to define an interface for the shape that extends TLShape. It must have a type property.

// BoxShape.ts
import type { TLShape } from '@tldraw/core'

export interface BoxShape extends TLShape {
  type: 'box'
  size: number[]
}

Component

Next, use TLShapeUtil.Component to create a second component for your shape's Component. The Renderer will use this component to display the shape on the canvas.

// BoxComponent.ts
import { SVGContainer, shapeComponent } from '@tldraw/core'
import * as React from 'react'
import type { BoxShape } from './BoxShape'

export const BoxComponent = TLShapeUtil.Component<BoxShape, SVGSVGElement>(
  ({ shape, events, meta }, ref) => {
    const color = meta.isDarkMode ? 'white' : 'black'

    return (
      <SVGContainer ref={ref} {...events}>
        <rect
          width={shape.size[0]}
          height={shape.size[1]}
          stroke={color}
          strokeWidth={2}
          strokeLinejoin="round"
          fill="none"
          pointerEvents="all"
        />
      </SVGContainer>
    )
  }
)

Your component can return HTML elements or SVG elements. If your shape is returning only SVG elements, wrap it in an SVGContainer. If your shape returns HTML elements, wrap it in an HTMLContainer. Note that you must set pointerEvents manually on the shapes you wish to receive pointer events.

The component will receive the following props:

NameTypeDescription
shapeTLShapeThe shape from page.shapes that is being rendered
meta{}The value provided to the Renderer's meta prop
events{}Several pointer events that should be set on the container element
isSelectedbooleanThe shape is selected (its id is in pageState.selectedIds)
isHoveredbooleanThe shape is hovered (its id is pageState.hoveredId)
isEditingbooleanThe shape is being edited (its id is pageState.editingId)
isGhostbooleanThe shape is ghosted or is the child of a ghosted shape.
isChildOfSelectedbooleanThe shape is the child of a selected shape.
onShapeChangeFunctionThe callback provided to the Renderer's onShapeChange prop
onShapeBlurFunctionThe callback provided to the Renderer's onShapeBlur prop

Indicator

Next, use TLShapeUtil.Indicator to create a second component for your shape's Indicator. This component is shown when the shape is hovered or selected. Your Indicator must return SVG elements only.

// BoxIndicator.ts

export const BoxIndicator = TLShapeUtil.Indicator<BoxShape>(({ shape }) => {
  return (
    <rect
      fill="none"
      stroke="dodgerblue"
      strokeWidth={1}
      width={shape.size[0]}
      height={shape.size[1]}
    />
  )
})

The indicator component will receive the following props:

NameTypeDescription
shapeTLShapeThe shape from page.shapes that is being rendered
meta{}The value provided to the Renderer's meta prop
userTLUserThe user when shown in a multiplayer session
isSelectedbooleanWhether the current shape is selected (true if its id is in pageState.selectedIds)
isHoveredbooleanWhether the current shape is hovered (true if its id is pageState.hoveredId)

ShapeUtil

Next, create a "shape util" for your shape. This is a class that extends TLShapeUtil. The Renderer will use an instance of this class to answer questions about the shape: what it should look like, where it is on screen, whether it can rotate, etc.

// BoxUtil.ts
import { TLBounds, TLShapeUtil, Utils } from '@tldraw/core'
import { BoxComponent } from './BoxComponent'
import { BoxIndicator } from './BoxIndicator'
import type { BoxShape } from './BoxShape'

export class BoxUtil extends TLShapeUtil<BoxShape, SVGSVGElement> {
  Component = BoxComponent

  Indicator = BoxIndicator

  getBounds = (shape: BoxShape): TLBounds => {
    const [width, height] = shape.size

    const bounds = {
      minX: 0,
      maxX: width,
      minY: 0,
      maxY: height,
      width,
      height,
    }

    return Utils.translateBounds(bounds, shape.point)
  }
}

Set the Component field to your component and the Indicator field to your indicator component. Then define the getBounds method. This method will receive a shape and should return a TLBounds object.

You may also set the following fields:

NameTypeDefaultDescription
showCloneHandlesbooleanfalseWhether to display clone handles when the shape is the only selected shape
hideBoundsbooleanfalseWhether to hide the bounds when the shape is the only selected shape
isStatefulbooleanfalseWhether the shape has its own React state. When true, the shape will not be unmounted when off-screen

ShapeUtils Object

Finally, create a mapping of your project's shape utils and the type properties of their corresponding shapes. Pass this object to the Renderer's shapeUtils prop.

// App.tsx

const shapeUtils = {
  box: new BoxUtil(),
  circle: new CircleUtil(),
  text: new TextUtil(),
}

export function App() {
  // ...

  return <Renderer page={page} pageState={pageState} {...etc} shapeUtils={shapeUtils} />
}

Local Development

To start the development servers for the package and the advanced example:

  • Run yarn to install dependencies.
  • Run yarn start.
  • Open localhost:5420.

You can also run:

  • start:advanced to start development servers for the package and the advanced example.
  • start:simple to start development servers for the package and the simple example.
  • test to execute unit tests via Jest.
  • docs to build the docs via ts-doc.
  • build to build the package.

Example

See the example folder or this CodeSandbox example.

Community

Support

Need help? Please open an issue for support.

Discussion

Want to connect with other devs? Visit the Discord channel.

License

This project is licensed under MIT.

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

Author