[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:
parent
b9d8246629
commit
1367e4c500
10 changed files with 193 additions and 3 deletions
87
apps/examples/src/examples/OnTheCanvas.tsx
Normal file
87
apps/examples/src/examples/OnTheCanvas.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 />
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import { ComponentType } from 'react'
|
||||
|
||||
/** @public */
|
||||
export type TLInFrontOfTheCanvas = ComponentType<object>
|
|
@ -0,0 +1,4 @@
|
|||
import { ComponentType } from 'react'
|
||||
|
||||
/** @public */
|
||||
export type TLOnTheCanvas = ComponentType<object>
|
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue