Custom renderer example (#3091)
This PR adds a custom renderer example. Ever wanted to see how to use an HTML canvas with tldraw? Here's how! ![Kapture 2024-03-09 at 22 35 09](https://github.com/tldraw/tldraw/assets/23072548/9e258a8f-f99f-419a-b92a-f58b1ce93973) ### Change Type - [x] `documentation` — Changes to the documentation only[^2]
This commit is contained in:
parent
eb80cf787b
commit
a691c60315
3 changed files with 151 additions and 0 deletions
107
apps/examples/src/examples/custom-renderer/CustomRenderer.tsx
Normal file
107
apps/examples/src/examples/custom-renderer/CustomRenderer.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { useLayoutEffect, useRef } from 'react'
|
||||||
|
import { TLDrawShape, TLGeoShape, getDefaultColorTheme, useEditor } from 'tldraw'
|
||||||
|
|
||||||
|
export function CustomRenderer() {
|
||||||
|
const editor = useEditor()
|
||||||
|
const rCanvas = useRef<HTMLCanvasElement>(null)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const canvas = rCanvas.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
canvas.style.width = '100%'
|
||||||
|
canvas.style.height = '100%'
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
|
||||||
|
canvas.width = rect.width
|
||||||
|
canvas.height = rect.height
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
|
||||||
|
let isCancelled = false
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (isCancelled) return
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
ctx.resetTransform()
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
const camera = editor.getCamera()
|
||||||
|
ctx.scale(camera.z, camera.z)
|
||||||
|
ctx.translate(camera.x, camera.y)
|
||||||
|
|
||||||
|
const renderingShapes = editor.getRenderingShapes()
|
||||||
|
const theme = getDefaultColorTheme({ isDarkMode: editor.user.getIsDarkMode() })
|
||||||
|
const currentPageId = editor.getCurrentPageId()
|
||||||
|
|
||||||
|
for (const { shape, maskedPageBounds, opacity } of renderingShapes) {
|
||||||
|
if (!maskedPageBounds) continue
|
||||||
|
ctx.save()
|
||||||
|
|
||||||
|
if (shape.parentId !== currentPageId) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.rect(
|
||||||
|
maskedPageBounds.minX,
|
||||||
|
maskedPageBounds.minY,
|
||||||
|
maskedPageBounds.width,
|
||||||
|
maskedPageBounds.height
|
||||||
|
)
|
||||||
|
ctx.clip()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
|
||||||
|
ctx.globalAlpha = opacity
|
||||||
|
|
||||||
|
const transform = editor.getShapePageTransform(shape.id)
|
||||||
|
ctx.transform(transform.a, transform.b, transform.c, transform.d, transform.e, transform.f)
|
||||||
|
|
||||||
|
if (editor.isShapeOfType<TLDrawShape>(shape, 'draw')) {
|
||||||
|
// Draw a freehand shape
|
||||||
|
for (const segment of shape.props.segments) {
|
||||||
|
ctx.moveTo(segment.points[0].x, segment.points[0].y)
|
||||||
|
if (segment.type === 'straight') {
|
||||||
|
ctx.lineTo(segment.points[1].x, segment.points[1].y)
|
||||||
|
} else {
|
||||||
|
for (const point of segment.points.slice(1)) {
|
||||||
|
ctx.lineTo(point.x, point.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = theme[shape.props.color].solid
|
||||||
|
ctx.lineWidth = 4
|
||||||
|
ctx.stroke()
|
||||||
|
if (shape.props.fill !== 'none' && shape.props.isClosed) {
|
||||||
|
ctx.fillStyle = theme[shape.props.color].semi
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
} else if (editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
|
||||||
|
// Draw a geo shape
|
||||||
|
const bounds = editor.getShapeGeometry(shape).bounds
|
||||||
|
ctx.strokeStyle = theme[shape.props.color].solid
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.strokeRect(bounds.minX, bounds.minY, bounds.width, bounds.height)
|
||||||
|
} else {
|
||||||
|
// Draw any other kind of shape
|
||||||
|
const bounds = editor.getShapeGeometry(shape).bounds
|
||||||
|
ctx.strokeStyle = 'black'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.strokeRect(bounds.minX, bounds.minY, bounds.width, bounds.height)
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(render)
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true
|
||||||
|
}
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
return <canvas ref={rCanvas} />
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useLayoutEffect } from 'react'
|
||||||
|
import { DefaultCanvas, Tldraw } from 'tldraw'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
|
import { CustomRenderer } from './CustomRenderer'
|
||||||
|
|
||||||
|
export default function CustomRendererExample() {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// Hide the regular shapes layer using CSS.
|
||||||
|
const script = document.createElement('style')
|
||||||
|
if (!script) return
|
||||||
|
script.innerHTML = `.tl-shapes { display: none; }`
|
||||||
|
document.body.appendChild(script)
|
||||||
|
return () => {
|
||||||
|
script.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
persistenceKey="example"
|
||||||
|
components={{
|
||||||
|
// We're replacing the Background component with our custom renderer
|
||||||
|
Background: CustomRenderer,
|
||||||
|
// Even though we're hiding the shapes, we'll still do a bunch of work
|
||||||
|
// in react to figure out which shapes to create. In reality, you might
|
||||||
|
// want to set the Canvas component to null and render it all yourself.
|
||||||
|
Canvas: DefaultCanvas,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
11
apps/examples/src/examples/custom-renderer/README.md
Normal file
11
apps/examples/src/examples/custom-renderer/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
title: Custom renderer
|
||||||
|
component: ./CustomRendererExample.tsx
|
||||||
|
category: basic
|
||||||
|
---
|
||||||
|
|
||||||
|
You can _sort of_ use a custom renderer with tldraw.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This example shows how you might use a custom renderer with tldraw.
|
Loading…
Reference in a new issue