Shapes
In tldraw, shapes are the things that are on the canvas. This article is about shapes: what they are, how they work, and how to create your own shapes. If you'd prefer to see an example, see the tldraw repository's examples app for examples of how to create custom shapes in tldraw.
Custom shapes
Let's create a custom "card" shape.
Shape type
In tldraw's data model, each shape is represented by a JSON object. Let's first create a type that describes what this object will look like.
import { TLBaseShape } from '@tldraw/tldraw'
type CardShape = TLBaseShape<
'card',
{ w: number, h: number }
>
With the TLBaseShape
helper, we define the shape's type
property (card
) and the shape's props
property ({ w: number, h: number }
). The type can be any string but the props must be a regular JSON-serializable JavaScript object.
The TLBaseShape
helper adds the other default properties of a shape, such as parentId
, x
, y
, and rotation
.
Shape Util
While tldraw's shapes themselves are simple JSON objects, we use ShapeUtil
classes to answer questions about shapes. For example, when the editor needs to know the bounding box of our card shape, it will find a ShapeUtil
for the card
type and call that util's bounds
method, passing in the CardShape
object as an argument.
Let's create a ShapeUtil
class for the shape.
import { ShapeUtil, HTMLContainer } from '@tldraw/tldraw'
class CardShapeUtil extends ShapeUtil<CardShape> {
static type = 'card' as const
getDefaultProps(): CardShape['props'] {
return {
w: 100,
h: 100,
}
}
getBounds(shape: Shape) {
return new Box2d(0, 0, shape.props.w, shape.props.h)
}
component(shape: Shape) {
return (
<HTMLContainer>Hello</HTMLContainer>
)
}
indicator(shape: Shape) {
return (
<rect width={shape.props.w} height={shape.props.h}/>
)
}
}
This is a minimal ShapeUtil
. We've given it a static property type
that matches the type of our shape, we've provided implementations for the abstract methods getDefaultProps
, getBounds
, component
, and indicator
.
We still have work to do on the CardShapeUtil
class, but we'll come back to it later. For now, let's put the shape onto the canvas by passing it to the <Tldraw>
component.
Defining the shape
Before we pass the shape down, we need to package it up in a way using the defineShape
function. We can then create an array of our defined shapes and pass them into the <Tldraw>
component's shapes
prop.
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
const MyCardShape = defineShape('card', { util: CardShapeUtil })
const MyCustomShapes = [MyCardShape]
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapes={MyCustomShapes}/>
</div>
)
}
The defineShape
function can also be used to include a tool that we can use to create this type of shape. For now, let's create it using the Editor
API.
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapes={MyCustomShapes} onMount={editor => {
editor.createShapes([{ type: "card" }])
}}/>
</div>
)
}
Once the page refreshes, we should now have our custom shape on the canvas.
Using starter shapes
You can use "starter" shape utils like BaseBoxShapeUtil
to get regular rectangular shape behavior.
todo
Flags
You can use flags like hideRotateHandle
to hide different parts of the UI when the shape is selected, or else to control different behaviors of the shape.
todo
Interaction
You can turn on pointer-events
to allow users to interact inside of the shape.
todo
Editing
You can make shapes "editable" to help decide when they're interactive or not.
todo