[feature] Things on the canvas (#2150)

This PR adds two new component overrides to the editor's `components`
slot. They are:

- `<OnTheCanvas/>`, which renders inside of the html layer that scales
and translates with the camera
- `<InFrontOfTheCanvas/>`, which renders in front of the canvas but
behind any UI elements, and which does not scale / pan with the camera.

![Kapture 2023-11-06 at 12 19
15](https://github.com/tldraw/tldraw/assets/23072548/51c0421d-8b39-48b5-9b8a-c717253c3423)

### Change Type

- [x] `minor` — New feature

### Test Plan

1. See the "on the canvas" example.

### Release Notes

- [editor] Adds two new components, `OnTheCanvas` and
`InFrontOfTheCanvas`.
This commit is contained in:
Steve Ruiz 2023-11-07 09:27:20 +00:00 committed by GitHub
parent b9d8246629
commit 1367e4c500
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 193 additions and 3 deletions

View file

@ -0,0 +1,87 @@
import { stopEventPropagation, Tldraw, TLEditorComponents, track, useEditor } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useState } from 'react'
// The "OnTheCanvas" component is rendered on top of the canvas, but behind the UI.
function MyComponent() {
const [state, setState] = useState(0)
return (
<>
<div
style={{
position: 'absolute',
top: 50,
left: 50,
width: 'fit-content',
padding: 12,
borderRadius: 8,
backgroundColor: 'goldenrod',
zIndex: 0,
pointerEvents: 'all',
userSelect: 'unset',
}}
onPointerDown={stopEventPropagation}
onPointerMove={stopEventPropagation}
>
The count is {state}! <button onClick={() => setState((s) => s - 1)}>+1</button>
</div>
<div
style={{
position: 'absolute',
top: 150,
left: 150,
width: 128,
padding: 12,
borderRadius: 8,
backgroundColor: 'pink',
zIndex: 99999999,
pointerEvents: 'all',
userSelect: 'unset',
}}
onPointerDown={stopEventPropagation}
onPointerMove={stopEventPropagation}
>
The count is {state}! <button onClick={() => setState((s) => s + 1)}>+1</button>
</div>
</>
)
}
// The "InFrontOfTheCanvas" component is rendered on top of the canvas, but behind the UI.
const MyComponentInFront = track(() => {
const editor = useEditor()
const { selectionRotatedPageBounds } = editor
if (!selectionRotatedPageBounds) return null
const pageCoordinates = editor.pageToScreen(selectionRotatedPageBounds.point)
return (
<div
style={{
position: 'absolute',
top: Math.max(64, pageCoordinates.y - 64),
left: Math.max(64, pageCoordinates.x),
padding: 12,
background: '#efefef',
}}
>
This does not scale with the zoom
</div>
)
})
const components: Partial<TLEditorComponents> = {
OnTheCanvas: MyComponent,
InFrontOfTheCanvas: MyComponentInFront,
SnapLine: null,
}
export default function OnTheCanvasExample() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="things-on-the-canvas-example" components={components} />
</div>
)
}

View file

@ -26,6 +26,7 @@ import ExternalContentSourcesExample from './examples/ExternalContentSourcesExam
import HideUiExample from './examples/HideUiExample'
import MetaExample from './examples/MetaExample'
import MultipleExample from './examples/MultipleExample'
import OnTheCanvasExample from './examples/OnTheCanvas'
import PersistenceExample from './examples/PersistenceExample'
import ReadOnlyExample from './examples/ReadOnlyExample'
import ScrollExample from './examples/ScrollExample'
@ -84,6 +85,11 @@ export const allExamples: Example[] = [
path: 'readonly',
element: <ReadOnlyExample />,
},
{
title: 'Things on the canvas',
path: 'things-on-the-canvas',
element: <OnTheCanvasExample />,
},
{
title: 'Scroll example',
path: 'scroll',

View file

@ -2244,6 +2244,9 @@ export type TLHoveredShapeIndicatorComponent = ComponentType<{
shapeId: TLShapeId;
}>;
// @public (undocumented)
export type TLInFrontOfTheCanvas = ComponentType<object>;
// @public (undocumented)
export type TLInterruptEvent = (info: TLInterruptEventInfo) => void;
@ -2322,6 +2325,9 @@ export type TLOnRotateHandler<T extends TLShape> = TLEventChangeHandler<T>;
// @public (undocumented)
export type TLOnRotateStartHandler<T extends TLShape> = TLEventStartHandler<T>;
// @public (undocumented)
export type TLOnTheCanvas = ComponentType<object>;
// @public (undocumented)
export type TLOnTranslateEndHandler<T extends TLShape> = TLEventChangeHandler<T>;

View file

@ -37949,6 +37949,37 @@
"endIndex": 5
}
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLInFrontOfTheCanvas:type",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export type TLInFrontOfTheCanvas = "
},
{
"kind": "Reference",
"text": "ComponentType",
"canonicalReference": "@types/react!React.ComponentType:type"
},
{
"kind": "Content",
"text": "<object>"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/components/default-components/DefaultInFrontOfTheCanvas.tsx",
"releaseTag": "Public",
"name": "TLInFrontOfTheCanvas",
"typeTokenRange": {
"startIndex": 1,
"endIndex": 3
}
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLInterruptEvent:type",
@ -39009,6 +39040,37 @@
"endIndex": 5
}
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLOnTheCanvas:type",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export type TLOnTheCanvas = "
},
{
"kind": "Reference",
"text": "ComponentType",
"canonicalReference": "@types/react!React.ComponentType:type"
},
{
"kind": "Content",
"text": "<object>"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/components/default-components/DefaultOnTheCanvas.tsx",
"releaseTag": "Public",
"name": "TLOnTheCanvas",
"typeTokenRange": {
"startIndex": 1,
"endIndex": 3
}
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLOnTranslateEndHandler:type",

View file

@ -73,6 +73,8 @@ export {
DefaultHoveredShapeIndicator,
type TLHoveredShapeIndicatorComponent,
} from './lib/components/default-components/DefaultHoveredShapeIndicator'
export { type TLInFrontOfTheCanvas } from './lib/components/default-components/DefaultInFrontOfTheCanvas'
export { type TLOnTheCanvas } from './lib/components/default-components/DefaultOnTheCanvas'
export {
DefaultScribble,
type TLScribbleComponent,

View file

@ -109,6 +109,7 @@ export function Canvas({ className }: { className?: string }) {
</defs>
</svg>
<div ref={rHtmlLayer} className="tl-html-layer tl-shapes" draggable={false}>
<OnTheCanvasWrapper />
<SelectionBackgroundWrapper />
{hideShapes ? null : debugSvg ? <ShapesWithSVGs /> : <ShapesToDisplay />}
</div>
@ -126,6 +127,7 @@ export function Canvas({ className }: { className?: string }) {
<SelectionForegroundWrapper />
<LiveCollaborators />
</div>
<InFrontOfTheCanvasWrapper />
</div>
</div>
)
@ -518,3 +520,15 @@ export function SelectionBackgroundWrapper() {
if (!selectionBounds || !SelectionBackground) return null
return <SelectionBackground bounds={selectionBounds} rotation={selectionRotation} />
}
export function OnTheCanvasWrapper() {
const { OnTheCanvas } = useEditorComponents()
if (!OnTheCanvas) return null
return <OnTheCanvas />
}
export function InFrontOfTheCanvasWrapper() {
const { InFrontOfTheCanvas } = useEditorComponents()
if (!InFrontOfTheCanvas) return null
return <InFrontOfTheCanvas />
}

View file

@ -0,0 +1,4 @@
import { ComponentType } from 'react'
/** @public */
export type TLInFrontOfTheCanvas = ComponentType<object>

View file

@ -0,0 +1,4 @@
import { ComponentType } from 'react'
/** @public */
export type TLOnTheCanvas = ComponentType<object>

View file

@ -21,6 +21,8 @@ import {
DefaultHoveredShapeIndicator,
TLHoveredShapeIndicatorComponent,
} from '../components/default-components/DefaultHoveredShapeIndicator'
import { TLInFrontOfTheCanvas } from '../components/default-components/DefaultInFrontOfTheCanvas'
import { TLOnTheCanvas } from '../components/default-components/DefaultOnTheCanvas'
import {
DefaultScribble,
TLScribbleComponent,
@ -48,7 +50,7 @@ import {
import { DefaultSpinner, TLSpinnerComponent } from '../components/default-components/DefaultSpinner'
import { DefaultSvgDefs, TLSvgDefsComponent } from '../components/default-components/DefaultSvgDefs'
interface BaseEditorComponents {
export interface BaseEditorComponents {
Background: TLBackgroundComponent
SvgDefs: TLSvgDefsComponent
Brush: TLBrushComponent
@ -68,6 +70,8 @@ interface BaseEditorComponents {
SelectionForeground: TLSelectionForegroundComponent
SelectionBackground: TLSelectionBackgroundComponent
HoveredShapeIndicator: TLHoveredShapeIndicatorComponent
OnTheCanvas: TLOnTheCanvas
InFrontOfTheCanvas: TLInFrontOfTheCanvas
}
/** @public */
@ -113,6 +117,8 @@ export function EditorComponentsProvider({ overrides, children }: ComponentsCont
SelectionBackground: DefaultSelectionBackground,
SelectionForeground: DefaultSelectionForeground,
HoveredShapeIndicator: DefaultHoveredShapeIndicator,
OnTheCanvas: null,
InFrontOfTheCanvas: null,
...overrides,
}),
[overrides]

View file

@ -14583,7 +14583,7 @@
"text": "export interface TLUiContextMenuProps "
}
],
"fileUrlPath": "packages/tldraw/.tsbuild-api/lib/ui/components/ContextMenu.d.ts",
"fileUrlPath": "packages/tldraw/src/lib/ui/components/ContextMenu.tsx",
"releaseTag": "Public",
"name": "TLUiContextMenuProps",
"preserveMemberOrder": false,
@ -14606,7 +14606,6 @@
"text": ";"
}
],
"fileUrlPath": "packages/tldraw/src/lib/ui/components/ContextMenu.tsx",
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",