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