Add component for viewing an image of a snapshot (#2804)

This PR adds the `TldrawImage` component that displays a tldraw snapshot
as an SVG image.

![2024-02-15 at 12 29 52 - Coral
Cod](https://github.com/tldraw/tldraw/assets/15892272/14140e9e-7d6d-4dd3-88a3-86a6786325c5)

## Why

We've seen requests for this kind of thing from users. eg: GitBook, and
on discord:

<img width="710" alt="image"
src="https://github.com/tldraw/tldraw/assets/15892272/3d3a3e9d-66b9-42e7-81de-a70aa7165bdc">

The component provides a way to do that.
This PR also untangles various bits of editor state from image
exporting, which makes it easier for library users to export images more
agnostically. (ie: they can now export any shapes on any page in any
theme. previously, they had to change the user's state to do that).

## What else

- This PR also adds an **Image snapshot** example to demonstrate the new
component.
- We now pass an `isDarkMode` property to the `toSvg` method (inside the
`ctx` argument). This means that `toSvg` doesn't have to rely on editor
state anymore. I updated all our `toSvg` methods to use it.
- See code comments for more info.

## Any issues?

When you toggle to editing mode in the new example, text measurements
are initially wrong (until you edit the size of a text shape). Click on
the text shape to see how its indicator is wrong. Not sure why this is,
or if it's even related. Does it ring a bell with anyone? If not, I'll
take a closer look. (fixed, see comments --steve)

## Future work

Now that we've untangled image exporting from editor state, we could
expose some more helpful helpers for making this easier.

Fixes tld-2122

### Change Type

- [x] `minor` — New feature

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### Test Plan

1. Open the **Image snapshot** example.
2. Try editing the image, saving the image, and making sure the image
updates.

- [ ] Unit Tests
- [ ] End to end tests

### Release Notes

- Dev: Added the `TldrawImage` component.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Lu Wilson 2024-02-16 13:54:48 +00:00 committed by GitHub
parent dd67577fea
commit 212eb88480
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1159 additions and 70 deletions

View file

@ -0,0 +1,10 @@
---
title: Tldraw image component
component: ./TldrawImageExample.tsx
category: ui
priority: 3
---
Display a tldraw snapshot as an image by using the `TldrawImage` component.
---

View file

@ -0,0 +1,107 @@
import { Box, Editor, StoreSnapshot, TLPageId, TLRecord, Tldraw, TldrawImage } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useState } from 'react'
import initialSnapshot from './snapshot.json'
// There's a guide at the bottom of this file!
export default function TldrawImageExample() {
const [editor, setEditor] = useState<Editor>()
const [snapshot, setSnapshot] = useState<StoreSnapshot<TLRecord>>(initialSnapshot)
const [currentPageId, setCurrentPageId] = useState<TLPageId | undefined>()
const [showBackground, setShowBackground] = useState(true)
const [isDarkMode, setIsDarkMode] = useState(false)
const [viewportPageBounds, setViewportPageBounds] = useState(new Box(0, 0, 600, 400))
const [isEditing, setIsEditing] = useState(false)
const [format, setFormat] = useState<'svg' | 'png'>('svg')
return (
<div style={{ padding: 30 }}>
<div>
<button
style={{ cursor: 'pointer', marginRight: 8 }}
onClick={() => {
if (isEditing) {
if (!editor) return
setIsDarkMode(editor.user.getIsDarkMode())
setShowBackground(editor.getInstanceState().exportBackground)
setViewportPageBounds(editor.getViewportPageBounds())
setCurrentPageId(editor.getCurrentPageId())
setSnapshot(editor.store.getSnapshot())
setIsEditing(false)
} else {
setIsEditing(true)
}
}}
>
{isEditing ? '✓ Save drawing' : '✎ Edit drawing'}
</button>
{!isEditing && (
<>
<label htmlFor="format" style={{ marginRight: 8 }}>
Format
</label>
<select
name="format"
value={format}
onChange={(e) => {
setFormat(e.currentTarget.value as 'svg' | 'png')
}}
>
<option value="svg">SVG</option>
<option value="png">PNG</option>
</select>
</>
)}
</div>
<div style={{ width: 600, height: 400, marginTop: 15 }}>
{isEditing ? (
<Tldraw
snapshot={snapshot}
onMount={(editor: Editor) => {
setEditor(editor)
editor.updateInstanceState({ isDebugMode: false })
editor.user.updateUserPreferences({ isDarkMode })
if (currentPageId) {
editor.setCurrentPage(currentPageId)
}
if (viewportPageBounds) {
editor.zoomToBounds(viewportPageBounds, { inset: 0 })
}
}}
/>
) : (
<TldrawImage
//[1]
snapshot={snapshot}
// [2]
pageId={currentPageId}
// [3]
background={showBackground}
darkMode={isDarkMode}
bounds={viewportPageBounds}
padding={0}
scale={1}
format={format}
/>
)}
</div>
</div>
)
}
/*
This example shows how to use the `TldrawImage` component to display a snapshot
as an image. The example also allows you to toggle between editing the snapshot
and viewing it.
[1] Pass your snapshot to the `snapshot` prop of the `TldrawImage` component.
[2] You can specify which page to display by using the `pageId` prop. By
default, the first page is shown.
[3] You can customize the appearance of the image by passing other props to the
`TldrawImage` component. For example, you can toggle the background, set the
dark mode, and specify the viewport bounds.
*/

View file

@ -0,0 +1,454 @@
{
"store": {
"document:document": {
"gridSize": 10,
"name": "",
"meta": {},
"id": "document:document",
"typeName": "document"
},
"page:3qj9EtNgqSCW_6knX2K9_": {
"meta": {},
"id": "page:3qj9EtNgqSCW_6knX2K9_",
"name": "Page 1",
"index": "a1",
"typeName": "page"
},
"asset:imageAssetA": {
"type": "image",
"props": {
"w": 1200,
"h": 800,
"name": "",
"isAnimated": false,
"mimeType": "png",
"src": ""
},
"meta": {},
"id": "asset:imageAssetA",
"typeName": "asset"
},
"shape:EHeAIsYe4xu1-kGxK-Tl_": {
"x": 108.01190683749454,
"y": 138.58783300957418,
"rotation": 0,
"isLocked": false,
"opacity": 1,
"meta": {},
"type": "draw",
"props": {
"segments": [
{
"type": "free",
"points": [
{ "x": 0, "y": 0, "z": 0.5 },
{ "x": -0.07, "y": 0.07, "z": 0.5 },
{ "x": -0.03, "y": 0.14, "z": 0.5 },
{ "x": 0.47, "y": 0.14, "z": 0.5 },
{ "x": 3.54, "y": 0.14, "z": 0.5 },
{ "x": 10.47, "y": -1.05, "z": 0.5 },
{ "x": 17.57, "y": -3.65, "z": 0.5 },
{ "x": 24.13, "y": -7.67, "z": 0.5 },
{ "x": 31.36, "y": -13.59, "z": 0.5 },
{ "x": 36.65, "y": -20.16, "z": 0.5 },
{ "x": 39.27, "y": -26.49, "z": 0.5 },
{ "x": 40.12, "y": -32.21, "z": 0.5 },
{ "x": 40.11, "y": -37.65, "z": 0.5 },
{ "x": 39.45, "y": -41.67, "z": 0.5 },
{ "x": 36.85, "y": -44.45, "z": 0.5 },
{ "x": 33.68, "y": -46.36, "z": 0.5 },
{ "x": 30.91, "y": -46.85, "z": 0.5 },
{ "x": 27.84, "y": -46.77, "z": 0.5 },
{ "x": 24.96, "y": -45.3, "z": 0.5 },
{ "x": 22.43, "y": -42.1, "z": 0.5 },
{ "x": 20.4, "y": -36.5, "z": 0.5 },
{ "x": 19.37, "y": -26.9, "z": 0.5 },
{ "x": 19.17, "y": -14.09, "z": 0.5 },
{ "x": 21.12, "y": 1.65, "z": 0.5 },
{ "x": 25.48, "y": 20.19, "z": 0.5 },
{ "x": 29.24, "y": 36.72, "z": 0.5 },
{ "x": 31.85, "y": 53.01, "z": 0.5 },
{ "x": 33.37, "y": 70.4, "z": 0.5 },
{ "x": 33.2, "y": 84.67, "z": 0.5 },
{ "x": 30.57, "y": 95.01, "z": 0.5 },
{ "x": 24.99, "y": 101.14, "z": 0.5 },
{ "x": 17.55, "y": 103.88, "z": 0.5 },
{ "x": 9.92, "y": 104.49, "z": 0.5 },
{ "x": 3.66, "y": 104.01, "z": 0.5 },
{ "x": -0.64, "y": 102.5, "z": 0.5 },
{ "x": -2.92, "y": 100.27, "z": 0.5 },
{ "x": -3.6, "y": 96.97, "z": 0.5 },
{ "x": -0.45, "y": 91.45, "z": 0.5 },
{ "x": 9.15, "y": 83.2, "z": 0.5 },
{ "x": 24.06, "y": 72.39, "z": 0.5 },
{ "x": 40.16, "y": 59.82, "z": 0.5 },
{ "x": 54.06, "y": 45.99, "z": 0.5 },
{ "x": 63.61, "y": 33.29, "z": 0.5 },
{ "x": 69.31, "y": 20.87, "z": 0.5 },
{ "x": 72.36, "y": 8.36, "z": 0.5 },
{ "x": 73.05, "y": -0.07, "z": 0.5 },
{ "x": 72.5, "y": -5.4, "z": 0.5 },
{ "x": 71, "y": -9.15, "z": 0.5 },
{ "x": 69.23, "y": -11.13, "z": 0.5 },
{ "x": 67.7, "y": -12.08, "z": 0.5 },
{ "x": 66.64, "y": -12.35, "z": 0.5 },
{ "x": 66.07, "y": -12.29, "z": 0.5 },
{ "x": 65.59, "y": -10.98, "z": 0.5 },
{ "x": 64.91, "y": -5.01, "z": 0.5 },
{ "x": 63.56, "y": 6.47, "z": 0.5 },
{ "x": 61.24, "y": 21.76, "z": 0.5 },
{ "x": 59.27, "y": 36.74, "z": 0.5 },
{ "x": 58.63, "y": 50.8, "z": 0.5 },
{ "x": 58.56, "y": 64.21, "z": 0.5 },
{ "x": 59.69, "y": 72.89, "z": 0.5 },
{ "x": 62.94, "y": 78.64, "z": 0.5 },
{ "x": 66.62, "y": 82.15, "z": 0.5 },
{ "x": 70.67, "y": 82.79, "z": 0.5 },
{ "x": 75.98, "y": 81.09, "z": 0.5 },
{ "x": 81.97, "y": 75.87, "z": 0.5 },
{ "x": 87.87, "y": 68.39, "z": 0.5 },
{ "x": 92.7, "y": 59.73, "z": 0.5 },
{ "x": 95.82, "y": 50.99, "z": 0.5 },
{ "x": 96.92, "y": 44.55, "z": 0.5 },
{ "x": 97.02, "y": 39.91, "z": 0.5 },
{ "x": 96.56, "y": 36.38, "z": 0.5 },
{ "x": 95.41, "y": 34.37, "z": 0.5 },
{ "x": 94.09, "y": 33.59, "z": 0.5 },
{ "x": 92.51, "y": 33.65, "z": 0.5 },
{ "x": 90.9, "y": 36.01, "z": 0.5 },
{ "x": 89.8, "y": 42.11, "z": 0.5 },
{ "x": 89.36, "y": 50.78, "z": 0.5 },
{ "x": 90.17, "y": 60.6, "z": 0.5 },
{ "x": 92.22, "y": 67.78, "z": 0.5 },
{ "x": 95.07, "y": 71.7, "z": 0.5 },
{ "x": 98.44, "y": 73.97, "z": 0.5 },
{ "x": 101.62, "y": 74.13, "z": 0.5 },
{ "x": 105.05, "y": 70.76, "z": 0.5 },
{ "x": 108.93, "y": 63.31, "z": 0.5 },
{ "x": 112.09, "y": 54.51, "z": 0.5 },
{ "x": 113.75, "y": 47.54, "z": 0.5 },
{ "x": 114.33, "y": 42.98, "z": 0.5 },
{ "x": 114.57, "y": 40.11, "z": 0.5 },
{ "x": 114.53, "y": 39.06, "z": 0.5 },
{ "x": 114.21, "y": 39.22, "z": 0.5 },
{ "x": 113.61, "y": 41.78, "z": 0.5 },
{ "x": 113.24, "y": 47.65, "z": 0.5 },
{ "x": 113.2, "y": 54.77, "z": 0.5 },
{ "x": 113.59, "y": 59.94, "z": 0.5 },
{ "x": 115.15, "y": 63.14, "z": 0.5 },
{ "x": 117.5, "y": 65.35, "z": 0.5 },
{ "x": 119.82, "y": 65.98, "z": 0.5 },
{ "x": 122.17, "y": 64.83, "z": 0.5 },
{ "x": 124.45, "y": 60.83, "z": 0.5 },
{ "x": 126.38, "y": 54.53, "z": 0.5 },
{ "x": 127.57, "y": 48.58, "z": 0.5 },
{ "x": 128.02, "y": 43.71, "z": 0.5 },
{ "x": 128.14, "y": 40, "z": 0.5 },
{ "x": 128.14, "y": 37.99, "z": 0.5 },
{ "x": 128.05, "y": 37.08, "z": 0.5 },
{ "x": 127.96, "y": 36.89, "z": 0.5 },
{ "x": 128.34, "y": 37.5, "z": 0.5 },
{ "x": 131.02, "y": 39.91, "z": 0.5 },
{ "x": 137.11, "y": 44.76, "z": 0.5 },
{ "x": 145.28, "y": 51.58, "z": 0.5 },
{ "x": 153.49, "y": 59.87, "z": 0.5 },
{ "x": 159.26, "y": 69.47, "z": 0.5 },
{ "x": 161.58, "y": 81.66, "z": 0.5 },
{ "x": 158.71, "y": 94.96, "z": 0.5 },
{ "x": 147.18, "y": 107.43, "z": 0.5 },
{ "x": 132.52, "y": 116.36, "z": 0.5 },
{ "x": 119.46, "y": 120.3, "z": 0.5 },
{ "x": 109.14, "y": 121.49, "z": 0.5 },
{ "x": 102.95, "y": 119.79, "z": 0.5 },
{ "x": 100.5, "y": 114.09, "z": 0.5 },
{ "x": 105.6, "y": 103.93, "z": 0.5 },
{ "x": 120.72, "y": 89.8, "z": 0.5 },
{ "x": 143.19, "y": 72.46, "z": 0.5 },
{ "x": 167.67, "y": 53.41, "z": 0.5 },
{ "x": 185.27, "y": 37.82, "z": 0.5 },
{ "x": 193.79, "y": 26.69, "z": 0.5 },
{ "x": 197.17, "y": 17.76, "z": 0.5 },
{ "x": 194.75, "y": 11.87, "z": 0.5 },
{ "x": 185.34, "y": 9.07, "z": 0.5 },
{ "x": 172.73, "y": 8.91, "z": 0.5 },
{ "x": 162.58, "y": 10.74, "z": 0.5 },
{ "x": 155.42, "y": 13.45, "z": 0.5 },
{ "x": 151.3, "y": 15.61, "z": 0.5 },
{ "x": 150.03, "y": 17.66, "z": 0.5 }
]
}
],
"color": "black",
"fill": "none",
"dash": "draw",
"size": "l",
"isComplete": true,
"isClosed": false,
"isPen": false
},
"parentId": "page:3qj9EtNgqSCW_6knX2K9_",
"index": "a1",
"id": "shape:EHeAIsYe4xu1-kGxK-Tl_",
"typeName": "shape"
},
"shape:v0c3Ac-kUqB5C8cLsyT_E": {
"x": 325.71484375,
"y": 165.9453125,
"rotation": 0,
"isLocked": false,
"opacity": 1,
"meta": {},
"type": "text",
"props": {
"color": "black",
"size": "m",
"w": 139.38919029117903,
"text": "hey hey hey",
"font": "draw",
"align": "middle",
"autoSize": false,
"scale": 1.178662627660688
},
"parentId": "page:3qj9EtNgqSCW_6knX2K9_",
"index": "a2",
"id": "shape:v0c3Ac-kUqB5C8cLsyT_E",
"typeName": "shape"
},
"page:2E1xHBVQtZUB5fzXfSUPl": {
"meta": {},
"id": "page:2E1xHBVQtZUB5fzXfSUPl",
"name": "Page 2",
"index": "a2",
"typeName": "page"
},
"shape:qZ9C8PqSv6tSWya7LpL1t": {
"x": 144.09765625,
"y": 139.14829380718655,
"rotation": 0,
"isLocked": false,
"opacity": 1,
"meta": {},
"type": "text",
"props": {
"color": "black",
"size": "m",
"w": 74.73082091700364,
"text": "Page",
"font": "draw",
"align": "middle",
"autoSize": false,
"scale": 1.6823620652062432
},
"parentId": "page:2E1xHBVQtZUB5fzXfSUPl",
"index": "a1",
"id": "shape:qZ9C8PqSv6tSWya7LpL1t",
"typeName": "shape"
},
"shape:935Jl5xP5gxCs4JGaE81D": {
"x": 293.97412290107434,
"y": 137.07222276594962,
"rotation": 0,
"isLocked": false,
"opacity": 1,
"meta": {},
"type": "draw",
"props": {
"segments": [
{
"type": "free",
"points": [
{ "x": 0, "y": 0, "z": 0.5 },
{ "x": -0.07, "y": 0, "z": 0.5 },
{ "x": -0.15, "y": -0.07, "z": 0.5 },
{ "x": -0.15, "y": -0.13, "z": 0.5 },
{ "x": -0.15, "y": -0.3, "z": 0.5 },
{ "x": -0.15, "y": -0.57, "z": 0.5 },
{ "x": -0.15, "y": -1.21, "z": 0.5 },
{ "x": -0.15, "y": -2.07, "z": 0.5 },
{ "x": -0.15, "y": -2.93, "z": 0.5 },
{ "x": -0.15, "y": -3.79, "z": 0.5 },
{ "x": 0.72, "y": -5.84, "z": 0.5 },
{ "x": 2.02, "y": -8.45, "z": 0.5 },
{ "x": 3.62, "y": -10.87, "z": 0.5 },
{ "x": 5.3, "y": -13.09, "z": 0.5 },
{ "x": 9.45, "y": -17.77, "z": 0.5 },
{ "x": 10.99, "y": -19.35, "z": 0.5 },
{ "x": 12.21, "y": -20.59, "z": 0.5 },
{ "x": 17.21, "y": -25.22, "z": 0.5 },
{ "x": 20.05, "y": -27.37, "z": 0.5 },
{ "x": 22.88, "y": -29.39, "z": 0.5 },
{ "x": 25.87, "y": -31.19, "z": 0.5 },
{ "x": 28.6, "y": -32.74, "z": 0.5 },
{ "x": 29.98, "y": -33.01, "z": 0.5 },
{ "x": 31.22, "y": -33.16, "z": 0.5 },
{ "x": 36.02, "y": -33.8, "z": 0.5 },
{ "x": 38.11, "y": -33.82, "z": 0.5 },
{ "x": 41.03, "y": -33.82, "z": 0.5 },
{ "x": 44.67, "y": -33.82, "z": 0.5 },
{ "x": 47.41, "y": -33.82, "z": 0.5 },
{ "x": 49.29, "y": -33.56, "z": 0.5 },
{ "x": 50.76, "y": -33.13, "z": 0.5 },
{ "x": 51.93, "y": -32.45, "z": 0.5 },
{ "x": 52.89, "y": -31.61, "z": 0.5 },
{ "x": 53.67, "y": -30.82, "z": 0.5 },
{ "x": 54.27, "y": -30.1, "z": 0.5 },
{ "x": 54.93, "y": -29.21, "z": 0.5 },
{ "x": 55.59, "y": -28.3, "z": 0.5 },
{ "x": 55.97, "y": -27.29, "z": 0.5 },
{ "x": 56.22, "y": -26.26, "z": 0.5 },
{ "x": 56.39, "y": -25.3, "z": 0.5 },
{ "x": 56.51, "y": -24.46, "z": 0.5 },
{ "x": 56.54, "y": -23.28, "z": 0.5 },
{ "x": 56.54, "y": -22.04, "z": 0.5 },
{ "x": 56.66, "y": -20.78, "z": 0.5 },
{ "x": 56.8, "y": -19.55, "z": 0.5 },
{ "x": 56.83, "y": -17.43, "z": 0.5 },
{ "x": 56.83, "y": -15.23, "z": 0.5 },
{ "x": 56.83, "y": -13.16, "z": 0.5 },
{ "x": 56.83, "y": -11.22, "z": 0.5 },
{ "x": 56.38, "y": -9.22, "z": 0.5 },
{ "x": 55.89, "y": -7.28, "z": 0.5 },
{ "x": 54.78, "y": -4.61, "z": 0.5 },
{ "x": 52.96, "y": -0.39, "z": 0.5 },
{ "x": 51.48, "y": 2.96, "z": 0.5 },
{ "x": 47.31, "y": 10.43, "z": 0.5 },
{ "x": 40.34, "y": 21.06, "z": 0.5 },
{ "x": 34.42, "y": 28.68, "z": 0.5 },
{ "x": 30.28, "y": 33.6, "z": 0.5 },
{ "x": 27.27, "y": 37.05, "z": 0.5 },
{ "x": 25.13, "y": 39.37, "z": 0.5 },
{ "x": 21.82, "y": 42.92, "z": 0.5 },
{ "x": 17.82, "y": 47.17, "z": 0.5 },
{ "x": 15.34, "y": 49.73, "z": 0.5 },
{ "x": 13.9, "y": 51.19, "z": 0.5 },
{ "x": 11.34, "y": 53.73, "z": 0.5 },
{ "x": 8.28, "y": 56.8, "z": 0.5 },
{ "x": 5.59, "y": 59.34, "z": 0.5 },
{ "x": 3.26, "y": 61.47, "z": 0.5 },
{ "x": 2.07, "y": 62.53, "z": 0.5 },
{ "x": 1.33, "y": 63.14, "z": 0.5 },
{ "x": 0, "y": 64.42, "z": 0.5 },
{ "x": -1.45, "y": 65.88, "z": 0.5 },
{ "x": -2.44, "y": 66.86, "z": 0.5 },
{ "x": -3.21, "y": 67.63, "z": 0.5 },
{ "x": -3.85, "y": 68.27, "z": 0.5 },
{ "x": -4.46, "y": 68.88, "z": 0.5 },
{ "x": -4.73, "y": 69.23, "z": 0.5 },
{ "x": -4.91, "y": 69.5, "z": 0.5 },
{ "x": -5.01, "y": 69.67, "z": 0.5 },
{ "x": -5.1, "y": 69.83, "z": 0.5 },
{ "x": -5.1, "y": 69.92, "z": 0.5 },
{ "x": -5.1, "y": 69.99, "z": 0.5 },
{ "x": -5.03, "y": 70, "z": 0.5 },
{ "x": -4.44, "y": 69.89, "z": 0.5 },
{ "x": -3.48, "y": 69.5, "z": 0.5 },
{ "x": -2.76, "y": 69.01, "z": 0.5 },
{ "x": -1.38, "y": 68.32, "z": 0.5 },
{ "x": 0.4, "y": 67.51, "z": 0.5 },
{ "x": 3.06, "y": 66.45, "z": 0.5 },
{ "x": 6.29, "y": 65.24, "z": 0.5 },
{ "x": 9.81, "y": 64.23, "z": 0.5 },
{ "x": 13.24, "y": 63.43, "z": 0.5 },
{ "x": 17.8, "y": 62.5, "z": 0.5 },
{ "x": 22.71, "y": 61.56, "z": 0.5 },
{ "x": 28.7, "y": 60.72, "z": 0.5 },
{ "x": 34.89, "y": 59.94, "z": 0.5 },
{ "x": 42.4, "y": 59.29, "z": 0.5 },
{ "x": 50.03, "y": 58.71, "z": 0.5 },
{ "x": 57.97, "y": 58.58, "z": 0.5 },
{ "x": 65.61, "y": 58.58, "z": 0.5 },
{ "x": 73.79, "y": 58.58, "z": 0.5 },
{ "x": 81.93, "y": 58.58, "z": 0.5 },
{ "x": 89.91, "y": 58.58, "z": 0.5 },
{ "x": 97.54, "y": 58.58, "z": 0.5 },
{ "x": 101.72, "y": 58.58, "z": 0.5 },
{ "x": 104.83, "y": 58.58, "z": 0.5 },
{ "x": 110.48, "y": 58.58, "z": 0.5 },
{ "x": 117.33, "y": 58.75, "z": 0.5 },
{ "x": 119.83, "y": 59.08, "z": 0.5 },
{ "x": 124.73, "y": 59.89, "z": 0.5 },
{ "x": 129.54, "y": 60.77, "z": 0.5 },
{ "x": 131.86, "y": 61.42, "z": 0.5 },
{ "x": 134.07, "y": 62.11, "z": 0.5 },
{ "x": 135.85, "y": 62.57, "z": 0.5 },
{ "x": 137.3, "y": 62.85, "z": 0.5 },
{ "x": 138.44, "y": 63.19, "z": 0.5 },
{ "x": 139.28, "y": 63.56, "z": 0.5 },
{ "x": 139.96, "y": 63.9, "z": 0.5 },
{ "x": 140.48, "y": 64.22, "z": 0.5 },
{ "x": 140.93, "y": 64.47, "z": 0.5 },
{ "x": 141.32, "y": 64.65, "z": 0.5 },
{ "x": 141.57, "y": 64.84, "z": 0.5 },
{ "x": 141.72, "y": 65.01, "z": 0.5 },
{ "x": 141.84, "y": 65.17, "z": 0.5 },
{ "x": 141.92, "y": 65.33, "z": 0.5 },
{ "x": 141.94, "y": 65.43, "z": 0.5 },
{ "x": 141.94, "y": 65.51, "z": 0.5 },
{ "x": 141.94, "y": 65.58, "z": 0.5 },
{ "x": 141.94, "y": 65.65, "z": 0.5 },
{ "x": 141.94, "y": 65.73, "z": 0.5 },
{ "x": 141.94, "y": 65.8, "z": 0.5 },
{ "x": 141.94, "y": 65.88, "z": 0.5 },
{ "x": 141.94, "y": 65.95, "z": 0.5 },
{ "x": 141.94, "y": 66.03, "z": 0.5 },
{ "x": 141.94, "y": 66.1, "z": 0.5 },
{ "x": 141.94, "y": 66.18, "z": 0.5 },
{ "x": 141.86, "y": 66.29, "z": 0.5 },
{ "x": 141.69, "y": 66.37, "z": 0.5 },
{ "x": 141.45, "y": 66.42, "z": 0.5 },
{ "x": 141.12, "y": 66.5, "z": 0.5 }
]
}
],
"color": "black",
"fill": "none",
"dash": "draw",
"size": "xl",
"isComplete": true,
"isClosed": false,
"isPen": false
},
"parentId": "page:2E1xHBVQtZUB5fzXfSUPl",
"index": "a2",
"id": "shape:935Jl5xP5gxCs4JGaE81D",
"typeName": "shape"
}
},
"schema": {
"schemaVersion": 1,
"storeVersion": 4,
"recordVersions": {
"asset": {
"version": 1,
"subTypeKey": "type",
"subTypeVersions": { "image": 3, "video": 3, "bookmark": 1 }
},
"camera": { "version": 1 },
"document": { "version": 2 },
"instance": { "version": 24 },
"instance_page_state": { "version": 5 },
"page": { "version": 1 },
"shape": {
"version": 3,
"subTypeKey": "type",
"subTypeVersions": {
"group": 0,
"text": 1,
"bookmark": 2,
"draw": 1,
"geo": 8,
"note": 5,
"line": 1,
"frame": 0,
"arrow": 3,
"highlight": 0,
"embed": 4,
"image": 3,
"video": 2
}
},
"instance_presence": { "version": 5 },
"pointer": { "version": 1 }
}
}
}

View file

@ -5,8 +5,6 @@ category: ui
priority: 3 priority: 3
--- ---
...
--- ---
The `Tldraw` component can be used inline with a set height and width. The `Tldraw` component can be used inline with a set height and width.

View file

@ -358,6 +358,12 @@ export function clockwiseAngleDist(a0: number, a1: number): number;
export { computed } export { computed }
// @internal (undocumented)
export function ContainerProvider({ container, children, }: {
container: HTMLDivElement;
children: React.ReactNode;
}): JSX_2.Element;
// @public (undocumented) // @public (undocumented)
export const coreShapes: readonly [typeof GroupShapeUtil]; export const coreShapes: readonly [typeof GroupShapeUtil];
@ -907,12 +913,18 @@ export class Editor extends EventEmitter<TLEventMap> {
visitDescendants(parent: TLPage | TLParentId | TLShape, visitor: (id: TLShapeId) => false | void): this; visitDescendants(parent: TLPage | TLParentId | TLShape, visitor: (id: TLShapeId) => false | void): this;
zoomIn(point?: Vec, animation?: TLAnimationOptions): this; zoomIn(point?: Vec, animation?: TLAnimationOptions): this;
zoomOut(point?: Vec, animation?: TLAnimationOptions): this; zoomOut(point?: Vec, animation?: TLAnimationOptions): this;
zoomToBounds(bounds: Box, targetZoom?: number, animation?: TLAnimationOptions): this; zoomToBounds(bounds: Box, opts?: {
targetZoom?: number;
inset?: number;
} & TLAnimationOptions): this;
zoomToContent(): this; zoomToContent(): this;
zoomToFit(animation?: TLAnimationOptions): this; zoomToFit(animation?: TLAnimationOptions): this;
zoomToSelection(animation?: TLAnimationOptions): this; zoomToSelection(animation?: TLAnimationOptions): this;
} }
// @internal (undocumented)
export const EditorContext: React_2.Context<Editor>;
// @public (undocumented) // @public (undocumented)
export class Ellipse2d extends Geometry2d { export class Ellipse2d extends Geometry2d {
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & { constructor(config: Omit<Geometry2dOptions, 'isClosed'> & {
@ -1813,6 +1825,7 @@ export type SVGContainerProps = React_3.HTMLAttributes<SVGElement>;
// @public (undocumented) // @public (undocumented)
export interface SvgExportContext { export interface SvgExportContext {
addExportDef(def: SvgExportDef): void; addExportDef(def: SvgExportDef): void;
readonly isDarkMode: boolean;
} }
// @public (undocumented) // @public (undocumented)

View file

@ -11234,7 +11234,7 @@
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getPageShapeIds:member(1)", "canonicalReference": "@tldraw/editor!Editor#getPageShapeIds:member(1)",
"docComment": "/**\n * Get the ids of shapes on a page.\n *\n * @param page - The page (or page id) to get.\n *\n * @example\n * ```ts\n * const idsOnPage1 = editor.getCurrentPageShapeIds('page1')\n * const idsOnPage2 = editor.getCurrentPageShapeIds(myPage2)\n * ```\n *\n * @public\n */\n", "docComment": "/**\n * Get the ids of shapes on a page.\n *\n * @param page - The page (or page id) to get.\n *\n * @example\n * ```ts\n * const idsOnPage1 = editor.getPageShapeIds('page1')\n * const idsOnPage2 = editor.getPageShapeIds(myPage2)\n * ```\n *\n * @public\n */\n",
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
@ -19171,7 +19171,7 @@
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#zoomToBounds:member(1)", "canonicalReference": "@tldraw/editor!Editor#zoomToBounds:member(1)",
"docComment": "/**\n * Zoom the camera to fit a bounding box (in the current page space).\n *\n * @param bounds - The bounding box.\n *\n * @param targetZoom - The desired zoom level. Defaults to 0.1.\n *\n * @param animation - The options for an animation.\n *\n * @example\n * ```ts\n * editor.zoomToBounds(myBounds)\n * editor.zoomToBounds(myBounds, 1)\n * editor.zoomToBounds(myBounds, 1, { duration: 100 })\n * ```\n *\n * @public\n */\n", "docComment": "/**\n * Zoom the camera to fit a bounding box (in the current page space).\n *\n * @param bounds - The bounding box.\n *\n * @param options - The options for an animation, target zoom, or custom inset amount.\n *\n * @example\n * ```ts\n * editor.zoomToBounds(myBounds)\n * editor.zoomToBounds(myBounds)\n * editor.zoomToBounds(myBounds, { duration: 100 })\n * editor.zoomToBounds(myBounds, { inset: 0, targetZoom: 1 })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
@ -19184,15 +19184,11 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": ", targetZoom?: " "text": ", opts?: "
}, },
{ {
"kind": "Content", "kind": "Content",
"text": "number" "text": "{\n targetZoom?: number;\n inset?: number;\n } & "
},
{
"kind": "Content",
"text": ", animation?: "
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -19214,8 +19210,8 @@
], ],
"isStatic": false, "isStatic": false,
"returnTypeTokenRange": { "returnTypeTokenRange": {
"startIndex": 7, "startIndex": 6,
"endIndex": 8 "endIndex": 7
}, },
"releaseTag": "Public", "releaseTag": "Public",
"isProtected": false, "isProtected": false,
@ -19230,18 +19226,10 @@
"isOptional": false "isOptional": false
}, },
{ {
"parameterName": "targetZoom", "parameterName": "opts",
"parameterTypeTokenRange": { "parameterTypeTokenRange": {
"startIndex": 3, "startIndex": 3,
"endIndex": 4 "endIndex": 5
},
"isOptional": true
},
{
"parameterName": "animation",
"parameterTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
}, },
"isOptional": true "isOptional": true
} }
@ -34180,6 +34168,33 @@
} }
], ],
"name": "addExportDef" "name": "addExportDef"
},
{
"kind": "PropertySignature",
"canonicalReference": "@tldraw/editor!SvgExportContext#isDarkMode:member",
"docComment": "/**\n * Whether the export should be in dark mode.\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "readonly isDarkMode: "
},
{
"kind": "Content",
"text": "boolean"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": true,
"isOptional": false,
"releaseTag": "Public",
"name": "isDarkMode",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
}
} }
], ],
"extendsTokenRanges": [] "extendsTokenRanges": []

View file

@ -253,9 +253,9 @@ export {
} from './lib/editor/types/history-types' } from './lib/editor/types/history-types'
export { type RequiredKeys, type TLSvgOptions } from './lib/editor/types/misc-types' export { type RequiredKeys, type TLSvgOptions } from './lib/editor/types/misc-types'
export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types' export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
export { useContainer } from './lib/hooks/useContainer' export { ContainerProvider, useContainer } from './lib/hooks/useContainer'
export { getCursor } from './lib/hooks/useCursor' export { getCursor } from './lib/hooks/useCursor'
export { useEditor } from './lib/hooks/useEditor' export { EditorContext, useEditor } from './lib/hooks/useEditor'
export type { TLEditorComponents } from './lib/hooks/useEditorComponents' export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
export { useShallowArrayIdentity, useShallowObjectIdentity } from './lib/hooks/useIdentity' export { useShallowArrayIdentity, useShallowObjectIdentity } from './lib/hooks/useIdentity'
export { useIsCropping } from './lib/hooks/useIsCropping' export { useIsCropping } from './lib/hooks/useIsCropping'

View file

@ -2176,7 +2176,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const bounds = this.getSelectionPageBounds() ?? this.getCurrentPageBounds() const bounds = this.getSelectionPageBounds() ?? this.getCurrentPageBounds()
if (bounds) { if (bounds) {
this.zoomToBounds(bounds, Math.min(1, this.getZoomLevel()), { duration: 220 }) this.zoomToBounds(bounds, { targetZoom: Math.min(1, this.getZoomLevel()), duration: 220 })
} }
return this return this
@ -2202,7 +2202,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (ids.length <= 0) return this if (ids.length <= 0) return this
const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id)))) const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
this.zoomToBounds(pageBounds, undefined, animation) this.zoomToBounds(pageBounds, animation)
return this return this
} }
@ -2333,7 +2333,10 @@ export class Editor extends EventEmitter<TLEventMap> {
const selectionPageBounds = this.getSelectionPageBounds() const selectionPageBounds = this.getSelectionPageBounds()
if (!selectionPageBounds) return this if (!selectionPageBounds) return this
this.zoomToBounds(selectionPageBounds, Math.max(1, this.getZoomLevel()), animation) this.zoomToBounds(selectionPageBounds, {
targetZoom: Math.max(1, this.getZoomLevel()),
...animation,
})
return this return this
} }
@ -2355,7 +2358,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const viewportPageBounds = this.getViewportPageBounds() const viewportPageBounds = this.getViewportPageBounds()
if (viewportPageBounds.h < selectionBounds.h || viewportPageBounds.w < selectionBounds.w) { if (viewportPageBounds.h < selectionBounds.h || viewportPageBounds.w < selectionBounds.w) {
this.zoomToBounds(selectionBounds, this.getCamera().z, animation) this.zoomToBounds(selectionBounds, { targetZoom: this.getCamera().z, ...animation })
return this return this
} else { } else {
@ -2398,22 +2401,25 @@ export class Editor extends EventEmitter<TLEventMap> {
* @example * @example
* ```ts * ```ts
* editor.zoomToBounds(myBounds) * editor.zoomToBounds(myBounds)
* editor.zoomToBounds(myBounds, 1) * editor.zoomToBounds(myBounds)
* editor.zoomToBounds(myBounds, 1, { duration: 100 }) * editor.zoomToBounds(myBounds, { duration: 100 })
* editor.zoomToBounds(myBounds, { inset: 0, targetZoom: 1 })
* ``` * ```
* *
* @param bounds - The bounding box. * @param bounds - The bounding box.
* @param targetZoom - The desired zoom level. Defaults to 0.1. * @param options - The options for an animation, target zoom, or custom inset amount.
* @param animation - The options for an animation.
* *
* @public * @public
*/ */
zoomToBounds(bounds: Box, targetZoom?: number, animation?: TLAnimationOptions): this { zoomToBounds(
bounds: Box,
opts?: { targetZoom?: number; inset?: number } & TLAnimationOptions
): this {
if (!this.getInstanceState().canMoveCamera) return this if (!this.getInstanceState().canMoveCamera) return this
const viewportScreenBounds = this.getViewportScreenBounds() const viewportScreenBounds = this.getViewportScreenBounds()
const inset = Math.min(256, viewportScreenBounds.width * 0.28) const inset = opts?.inset ?? Math.min(256, viewportScreenBounds.width * 0.28)
let zoom = clamp( let zoom = clamp(
Math.min( Math.min(
@ -2424,8 +2430,8 @@ export class Editor extends EventEmitter<TLEventMap> {
MAX_ZOOM MAX_ZOOM
) )
if (targetZoom !== undefined) { if (opts?.targetZoom !== undefined) {
zoom = Math.min(targetZoom, zoom) zoom = Math.min(opts.targetZoom, zoom)
} }
this.setCamera( this.setCamera(
@ -2434,7 +2440,7 @@ export class Editor extends EventEmitter<TLEventMap> {
y: -bounds.minY + (viewportScreenBounds.height - bounds.height * zoom) / 2 / zoom, y: -bounds.minY + (viewportScreenBounds.height - bounds.height * zoom) / 2 / zoom,
z: zoom, z: zoom,
}, },
animation opts
) )
return this return this
@ -3140,9 +3146,14 @@ export class Editor extends EventEmitter<TLEventMap> {
} }
} }
for (const childId of this.getSortedChildIdsForParent(this.getCurrentPageId())) { // If we're using editor state, then we're only interested in on-screen shapes.
// If we're not using the editor state, then we're interested in ALL shapes, even those from other pages.
const pages = useEditorState ? [this.getCurrentPage()] : this.getPages()
for (const page of pages) {
for (const childId of this.getSortedChildIdsForParent(page.id)) {
addShapeById(childId, 1, false) addShapeById(childId, 1, false)
} }
}
return renderingShapes return renderingShapes
} }
@ -3295,8 +3306,8 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @example * @example
* ```ts * ```ts
* const idsOnPage1 = editor.getCurrentPageShapeIds('page1') * const idsOnPage1 = editor.getPageShapeIds('page1')
* const idsOnPage2 = editor.getCurrentPageShapeIds(myPage2) * const idsOnPage2 = editor.getPageShapeIds(myPage2)
* ``` * ```
* *
* @param page - The page (or page id) to get. * @param page - The page (or page id) to get.
@ -8048,8 +8059,8 @@ export class Editor extends EventEmitter<TLEventMap> {
preserveAspectRatio = false, preserveAspectRatio = false,
} = opts } = opts
// todo: we shouldn't depend on the public theme here const isDarkMode = opts.darkMode ?? this.user.getIsDarkMode()
const theme = getDefaultColorTheme({ isDarkMode: this.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode })
// ---Figure out which shapes we need to include // ---Figure out which shapes we need to include
const shapeIdsToInclude = this.getShapeAndDescendantIds(ids) const shapeIdsToInclude = this.getShapeAndDescendantIds(ids)
@ -8127,6 +8138,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const exportDefPromisesById = new Map<string, Promise<void>>() const exportDefPromisesById = new Map<string, Promise<void>>()
const exportContext: SvgExportContext = { const exportContext: SvgExportContext = {
isDarkMode,
addExportDef: (def: SvgExportDef) => { addExportDef: (def: SvgExportDef) => {
if (exportDefPromisesById.has(def.key)) return if (exportDefPromisesById.has(def.key)) return
const promise = (async () => { const promise = (async () => {

View file

@ -11,4 +11,9 @@ export interface SvgExportContext {
* key. If multiple defs come with the same key, only one will be added. * key. If multiple defs come with the same key, only one will be added.
*/ */
addExportDef(def: SvgExportDef): void addExportDef(def: SvgExportDef): void
/**
* Whether the export should be in dark mode.
*/
readonly isDarkMode: boolean
} }

View file

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { Editor } from '../editor/Editor' import { Editor } from '../editor/Editor'
/** @internal */
export const EditorContext = React.createContext({} as Editor) export const EditorContext = React.createContext({} as Editor)
/** @public */ /** @public */

View file

@ -19,6 +19,7 @@ import { Editor } from '@tldraw/editor';
import { EMBED_DEFINITIONS } from '@tldraw/editor'; import { EMBED_DEFINITIONS } from '@tldraw/editor';
import { EmbedDefinition } from '@tldraw/editor'; import { EmbedDefinition } from '@tldraw/editor';
import { EnumStyleProp } from '@tldraw/editor'; import { EnumStyleProp } from '@tldraw/editor';
import { Expand } from '@tldraw/editor';
import { Geometry2d } from '@tldraw/editor'; import { Geometry2d } from '@tldraw/editor';
import { Group2d } from '@tldraw/editor'; import { Group2d } from '@tldraw/editor';
import { HandleSnapGeometry } from '@tldraw/editor'; import { HandleSnapGeometry } from '@tldraw/editor';
@ -91,6 +92,7 @@ import { TLOnResizeEndHandler } from '@tldraw/editor';
import { TLOnResizeHandler } from '@tldraw/editor'; import { TLOnResizeHandler } from '@tldraw/editor';
import { TLOnTranslateHandler } from '@tldraw/editor'; import { TLOnTranslateHandler } from '@tldraw/editor';
import { TLOnTranslateStartHandler } from '@tldraw/editor'; import { TLOnTranslateStartHandler } from '@tldraw/editor';
import { TLPageId } from '@tldraw/editor';
import { TLParentId } from '@tldraw/editor'; import { TLParentId } from '@tldraw/editor';
import { TLPointerEvent } from '@tldraw/editor'; import { TLPointerEvent } from '@tldraw/editor';
import { TLPointerEventInfo } from '@tldraw/editor'; import { TLPointerEventInfo } from '@tldraw/editor';
@ -534,7 +536,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
// (undocumented) // (undocumented)
providesBackgroundForChildren(): boolean; providesBackgroundForChildren(): boolean;
// (undocumented) // (undocumented)
toSvg(shape: TLFrameShape): Promise<SVGElement> | SVGElement; toSvg(shape: TLFrameShape, ctx: SvgExportContext): Promise<SVGElement> | SVGElement;
// (undocumented) // (undocumented)
static type: "frame"; static type: "frame";
} }
@ -698,6 +700,9 @@ export function getSvgAsImage(svg: SVGElement, isSafari: boolean, options: {
scale: number; scale: number;
}): Promise<Blob | null>; }): Promise<Blob | null>;
// @public (undocumented)
export function getSvgAsString(svg: SVGElement): Promise<string>;
// @public (undocumented) // @public (undocumented)
export class HandTool extends StateNode { export class HandTool extends StateNode {
// (undocumented) // (undocumented)
@ -764,7 +769,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
// (undocumented) // (undocumented)
toBackgroundSvg(shape: TLHighlightShape): SVGPathElement; toBackgroundSvg(shape: TLHighlightShape): SVGPathElement;
// (undocumented) // (undocumented)
toSvg(shape: TLHighlightShape): SVGPathElement; toSvg(shape: TLHighlightShape, ctx: SvgExportContext): SVGPathElement;
// (undocumented) // (undocumented)
static type: "highlight"; static type: "highlight";
} }
@ -891,7 +896,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
handles: DictValidator<IndexKey, VecModel>; handles: DictValidator<IndexKey, VecModel>;
}; };
// (undocumented) // (undocumented)
toSvg(shape: TLLineShape): SVGGElement; toSvg(shape: TLLineShape, ctx: SvgExportContext): SVGGElement;
// (undocumented) // (undocumented)
static type: "line"; static type: "line";
} }
@ -1199,6 +1204,28 @@ export const TldrawHandles: TLHandlesComponent;
// @public (undocumented) // @public (undocumented)
export const TldrawHoveredShapeIndicator: TLHoveredShapeIndicatorComponent; export const TldrawHoveredShapeIndicator: TLHoveredShapeIndicatorComponent;
// @public
export const TldrawImage: NamedExoticComponent< {
snapshot: StoreSnapshot<TLRecord>;
format?: "png" | "svg" | undefined;
pageId?: TLPageId | undefined;
shapeUtils?: readonly TLAnyShapeUtilConstructor[] | undefined;
bounds?: Box | undefined;
scale?: number | undefined;
background?: boolean | undefined;
padding?: number | undefined;
darkMode?: boolean | undefined;
preserveAspectRatio?: string | undefined;
}>;
// @public
export type TldrawImageProps = Expand<{
snapshot: StoreSnapshot<TLRecord>;
format?: 'png' | 'svg';
pageId?: TLPageId;
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
} & Partial<TLSvgOptions>>;
// @public (undocumented) // @public (undocumented)
export type TldrawProps = (Omit<TldrawUiProps, 'components'> & Omit<TldrawEditorBaseProps, 'components'> & { export type TldrawProps = (Omit<TldrawUiProps, 'components'> & Omit<TldrawEditorBaseProps, 'components'> & {
components?: TLComponents; components?: TLComponents;

View file

@ -6321,6 +6321,15 @@
"text": "TLFrameShape", "text": "TLFrameShape",
"canonicalReference": "@tldraw/tlschema!TLFrameShape:type" "canonicalReference": "@tldraw/tlschema!TLFrameShape:type"
}, },
{
"kind": "Content",
"text": ", ctx: "
},
{
"kind": "Reference",
"text": "SvgExportContext",
"canonicalReference": "@tldraw/editor!SvgExportContext:interface"
},
{ {
"kind": "Content", "kind": "Content",
"text": "): " "text": "): "
@ -6355,8 +6364,8 @@
], ],
"isStatic": false, "isStatic": false,
"returnTypeTokenRange": { "returnTypeTokenRange": {
"startIndex": 3, "startIndex": 5,
"endIndex": 8 "endIndex": 10
}, },
"releaseTag": "Public", "releaseTag": "Public",
"isProtected": false, "isProtected": false,
@ -6369,6 +6378,14 @@
"endIndex": 2 "endIndex": 2
}, },
"isOptional": false "isOptional": false
},
{
"parameterName": "ctx",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
} }
], ],
"isOptional": false, "isOptional": false,
@ -7721,6 +7738,57 @@
], ],
"name": "getSvgAsImage" "name": "getSvgAsImage"
}, },
{
"kind": "Function",
"canonicalReference": "@tldraw/tldraw!getSvgAsString:function(1)",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getSvgAsString(svg: "
},
{
"kind": "Reference",
"text": "SVGElement",
"canonicalReference": "!SVGElement:interface"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<string>"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/tldraw/src/lib/utils/export/export.ts",
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 5
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "svg",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"name": "getSvgAsString"
},
{ {
"kind": "Class", "kind": "Class",
"canonicalReference": "@tldraw/tldraw!HandTool:class", "canonicalReference": "@tldraw/tldraw!HandTool:class",
@ -8802,6 +8870,15 @@
"text": "TLHighlightShape", "text": "TLHighlightShape",
"canonicalReference": "@tldraw/tlschema!TLHighlightShape:type" "canonicalReference": "@tldraw/tlschema!TLHighlightShape:type"
}, },
{
"kind": "Content",
"text": ", ctx: "
},
{
"kind": "Reference",
"text": "SvgExportContext",
"canonicalReference": "@tldraw/editor!SvgExportContext:interface"
},
{ {
"kind": "Content", "kind": "Content",
"text": "): " "text": "): "
@ -8818,8 +8895,8 @@
], ],
"isStatic": false, "isStatic": false,
"returnTypeTokenRange": { "returnTypeTokenRange": {
"startIndex": 3, "startIndex": 5,
"endIndex": 4 "endIndex": 6
}, },
"releaseTag": "Public", "releaseTag": "Public",
"isProtected": false, "isProtected": false,
@ -8832,6 +8909,14 @@
"endIndex": 2 "endIndex": 2
}, },
"isOptional": false "isOptional": false
},
{
"parameterName": "ctx",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
} }
], ],
"isOptional": false, "isOptional": false,
@ -10650,6 +10735,15 @@
"text": "TLLineShape", "text": "TLLineShape",
"canonicalReference": "@tldraw/tlschema!TLLineShape:type" "canonicalReference": "@tldraw/tlschema!TLLineShape:type"
}, },
{
"kind": "Content",
"text": ", ctx: "
},
{
"kind": "Reference",
"text": "SvgExportContext",
"canonicalReference": "@tldraw/editor!SvgExportContext:interface"
},
{ {
"kind": "Content", "kind": "Content",
"text": "): " "text": "): "
@ -10666,8 +10760,8 @@
], ],
"isStatic": false, "isStatic": false,
"returnTypeTokenRange": { "returnTypeTokenRange": {
"startIndex": 3, "startIndex": 5,
"endIndex": 4 "endIndex": 6
}, },
"releaseTag": "Public", "releaseTag": "Public",
"isProtected": false, "isProtected": false,
@ -10680,6 +10774,14 @@
"endIndex": 2 "endIndex": 2
}, },
"isOptional": false "isOptional": false
},
{
"parameterName": "ctx",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
} }
], ],
"isOptional": false, "isOptional": false,
@ -13930,6 +14032,168 @@
"endIndex": 2 "endIndex": 2
} }
}, },
{
"kind": "Variable",
"canonicalReference": "@tldraw/tldraw!TldrawImage:var",
"docComment": "/**\n * A renderered SVG image of a Tldraw snapshot.\n *\n * @example\n * ```tsx\n * <TldrawImage snapshot={snapshot} />\n * \tsnapshot={snapshot}\n * \tpageId={pageId}\n * \tbackground={false}\n * darkMode={true}\n * bounds={new Box(0,0,600,400)}\n * scale={1}\n * />\n * ```\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "TldrawImage: "
},
{
"kind": "Content",
"text": "import(\"react\")."
},
{
"kind": "Reference",
"text": "NamedExoticComponent",
"canonicalReference": "@types/react!React.NamedExoticComponent:interface"
},
{
"kind": "Content",
"text": "<{\n snapshot: "
},
{
"kind": "Reference",
"text": "StoreSnapshot",
"canonicalReference": "@tldraw/store!StoreSnapshot:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLRecord",
"canonicalReference": "@tldraw/tlschema!TLRecord:type"
},
{
"kind": "Content",
"text": ">;\n format?: \"png\" | \"svg\" | undefined;\n pageId?: "
},
{
"kind": "Reference",
"text": "TLPageId",
"canonicalReference": "@tldraw/tlschema!TLPageId:type"
},
{
"kind": "Content",
"text": " | undefined;\n shapeUtils?: readonly "
},
{
"kind": "Reference",
"text": "TLAnyShapeUtilConstructor",
"canonicalReference": "@tldraw/editor!TLAnyShapeUtilConstructor:type"
},
{
"kind": "Content",
"text": "[] | undefined;\n bounds?: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
"text": "Box",
"canonicalReference": "@tldraw/editor!Box:class"
},
{
"kind": "Content",
"text": " | undefined;\n scale?: number | undefined;\n background?: boolean | undefined;\n padding?: number | undefined;\n darkMode?: boolean | undefined;\n preserveAspectRatio?: string | undefined;\n}>"
}
],
"fileUrlPath": "packages/tldraw/src/lib/TldrawImage.tsx",
"isReadonly": true,
"releaseTag": "Public",
"name": "TldrawImage",
"variableTypeTokenRange": {
"startIndex": 1,
"endIndex": 14
}
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/tldraw!TldrawImageProps:type",
"docComment": "/**\n * Props for the {@link @tldraw/tldraw#TldrawImage} component.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export type TldrawImageProps = "
},
{
"kind": "Reference",
"text": "Expand",
"canonicalReference": "@tldraw/utils!Expand:type"
},
{
"kind": "Content",
"text": "<{\n snapshot: "
},
{
"kind": "Reference",
"text": "StoreSnapshot",
"canonicalReference": "@tldraw/store!StoreSnapshot:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLRecord",
"canonicalReference": "@tldraw/tlschema!TLRecord:type"
},
{
"kind": "Content",
"text": ">;\n format?: 'png' | 'svg';\n pageId?: "
},
{
"kind": "Reference",
"text": "TLPageId",
"canonicalReference": "@tldraw/tlschema!TLPageId:type"
},
{
"kind": "Content",
"text": ";\n shapeUtils?: readonly "
},
{
"kind": "Reference",
"text": "TLAnyShapeUtilConstructor",
"canonicalReference": "@tldraw/editor!TLAnyShapeUtilConstructor:type"
},
{
"kind": "Content",
"text": "[];\n} & "
},
{
"kind": "Reference",
"text": "Partial",
"canonicalReference": "!Partial:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLSvgOptions",
"canonicalReference": "@tldraw/editor!TLSvgOptions:type"
},
{
"kind": "Content",
"text": ">>"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/tldraw/src/lib/TldrawImage.tsx",
"releaseTag": "Public",
"name": "TldrawImageProps",
"typeTokenRange": {
"startIndex": 1,
"endIndex": 15
}
},
{ {
"kind": "TypeAlias", "kind": "TypeAlias",
"canonicalReference": "@tldraw/tldraw!TldrawProps:type", "canonicalReference": "@tldraw/tldraw!TldrawProps:type",

View file

@ -3,6 +3,7 @@
// eslint-disable-next-line local/no-export-star // eslint-disable-next-line local/no-export-star
export * from '@tldraw/editor' export * from '@tldraw/editor'
export { Tldraw, type TldrawProps } from './lib/Tldraw' export { Tldraw, type TldrawProps } from './lib/Tldraw'
export { TldrawImage, type TldrawImageProps } from './lib/TldrawImage'
export { TldrawCropHandles, type TldrawCropHandlesProps } from './lib/canvas/TldrawCropHandles' export { TldrawCropHandles, type TldrawCropHandlesProps } from './lib/canvas/TldrawCropHandles'
export { TldrawHandles } from './lib/canvas/TldrawHandles' export { TldrawHandles } from './lib/canvas/TldrawHandles'
export { TldrawHoveredShapeIndicator } from './lib/canvas/TldrawHoveredShapeIndicator' export { TldrawHoveredShapeIndicator } from './lib/canvas/TldrawHoveredShapeIndicator'
@ -110,7 +111,7 @@ export {
} from './lib/utils/assets/assets' } from './lib/utils/assets/assets'
export { getEmbedInfo } from './lib/utils/embeds/embeds' export { getEmbedInfo } from './lib/utils/embeds/embeds'
export { copyAs } from './lib/utils/export/copyAs' export { copyAs } from './lib/utils/export/copyAs'
export { getSvgAsImage } from './lib/utils/export/export' export { getSvgAsImage, getSvgAsString } from './lib/utils/export/export'
export { exportAs } from './lib/utils/export/exportAs' export { exportAs } from './lib/utils/export/exportAs'
export { fitFrameToContent, removeFrame } from './lib/utils/frames/frames' export { fitFrameToContent, removeFrame } from './lib/utils/frames/frames'
export { setDefaultEditorAssetUrls } from './lib/utils/static-assets/assetUrls' export { setDefaultEditorAssetUrls } from './lib/utils/static-assets/assetUrls'

View file

@ -0,0 +1,178 @@
import {
Editor,
ErrorScreen,
Expand,
LoadingScreen,
StoreSnapshot,
TLAnyShapeUtilConstructor,
TLPageId,
TLRecord,
TLSvgOptions,
useShallowArrayIdentity,
useTLStore,
} from '@tldraw/editor'
import { memo, useLayoutEffect, useMemo, useState } from 'react'
import { defaultShapeUtils } from './defaultShapeUtils'
import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
import { getSvgAsImage, getSvgAsString } from './utils/export/export'
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
/**
* Props for the {@link @tldraw/tldraw#TldrawImage} component.
*
* @public
**/
export type TldrawImageProps = Expand<
{
/**
* The snapshot to display.
*/
snapshot: StoreSnapshot<TLRecord>
/**
* The image format to use. Defaults to 'svg'.
*/
format?: 'svg' | 'png'
/**
* The page to display. Defaults to the first page.
*/
pageId?: TLPageId
/**
* Additional shape utils to use.
*/
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
} & Partial<TLSvgOptions>
>
/**
* A renderered SVG image of a Tldraw snapshot.
*
* @example
* ```tsx
* <TldrawImage snapshot={snapshot} />
* snapshot={snapshot}
* pageId={pageId}
* background={false}
* darkMode={true}
* bounds={new Box(0,0,600,400)}
* scale={1}
* />
* ```
*
* @public
*/
export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
const [url, setUrl] = useState<string | null>(null)
const [container, setContainer] = useState<HTMLDivElement | null>(null)
const shapeUtils = useShallowArrayIdentity(props.shapeUtils ?? [])
const shapeUtilsWithDefaults = useMemo(() => [...defaultShapeUtils, ...shapeUtils], [shapeUtils])
const store = useTLStore({ snapshot: props.snapshot, shapeUtils: shapeUtilsWithDefaults })
const assets = useDefaultEditorAssetsWithOverrides()
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
const {
pageId,
bounds,
scale,
background,
padding,
darkMode,
preserveAspectRatio,
format = 'svg',
} = props
useLayoutEffect(() => {
if (!container) return
if (!store) return
if (!preloadingComplete) return
let isCancelled = false
const tempElm = document.createElement('div')
container.appendChild(tempElm)
container.classList.add('tl-container', 'tl-theme__light')
const editor = new Editor({
store,
shapeUtils: shapeUtilsWithDefaults ?? [],
tools: [],
getContainer: () => tempElm,
})
if (pageId) editor.setCurrentPage(pageId)
const shapeIds = editor.getCurrentPageShapeIds()
async function setSvg() {
const svg = await editor.getSvg([...shapeIds], {
bounds,
scale,
background,
padding,
darkMode,
preserveAspectRatio,
})
if (svg && !isCancelled) {
if (format === 'svg') {
const string = await getSvgAsString(svg)
if (!isCancelled) {
const blob = new Blob([string], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
setUrl(url)
}
} else if (format === 'png') {
const blob = await getSvgAsImage(svg, editor.environment.isSafari, {
type: format,
quality: 1,
scale: 2,
})
if (blob && !isCancelled) {
const url = URL.createObjectURL(blob)
setUrl(url)
}
}
}
editor.dispose()
}
setSvg()
return () => {
isCancelled = true
}
}, [
format,
container,
store,
shapeUtilsWithDefaults,
pageId,
bounds,
scale,
background,
padding,
darkMode,
preserveAspectRatio,
preloadingComplete,
preloadingError,
])
if (preloadingError) {
return <ErrorScreen>Could not load assets.</ErrorScreen>
}
if (!preloadingComplete) {
return <LoadingScreen>Loading assets...</LoadingScreen>
}
return (
<div ref={setContainer} style={{ position: 'relative', width: '100%', height: '100%' }}>
{url && <img src={url} style={{ width: '100%', height: '100%' }} />}
</div>
)
})

View file

@ -840,7 +840,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
} }
override toSvg(shape: TLArrowShape, ctx: SvgExportContext) { override toSvg(shape: TLArrowShape, ctx: SvgExportContext) {
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme)) ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
const color = theme[shape.props.color].solid const color = theme[shape.props.color].solid

View file

@ -187,7 +187,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
} }
override toSvg(shape: TLDrawShape, ctx: SvgExportContext) { override toSvg(shape: TLDrawShape, ctx: SvgExportContext) {
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme)) ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
const { color } = shape.props const { color } = shape.props

View file

@ -4,6 +4,7 @@ import {
Rectangle2d, Rectangle2d,
SVGContainer, SVGContainer,
SelectionEdge, SelectionEdge,
SvgExportContext,
TLFrameShape, TLFrameShape,
TLGroupShape, TLGroupShape,
TLOnResizeEndHandler, TLOnResizeEndHandler,
@ -96,8 +97,8 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
) )
} }
override toSvg(shape: TLFrameShape): SVGElement | Promise<SVGElement> { override toSvg(shape: TLFrameShape, ctx: SvgExportContext): SVGElement | Promise<SVGElement> {
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g') const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')

View file

@ -618,7 +618,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
override toSvg(shape: TLGeoShape, ctx: SvgExportContext) { override toSvg(shape: TLGeoShape, ctx: SvgExportContext) {
const { id, props } = shape const { id, props } = shape
const strokeWidth = STROKE_SIZES[props.size] const strokeWidth = STROKE_SIZES[props.size]
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme)) ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
let svgElm: SVGElement let svgElm: SVGElement

View file

@ -4,6 +4,7 @@ import {
Polygon2d, Polygon2d,
SVGContainer, SVGContainer,
ShapeUtil, ShapeUtil,
SvgExportContext,
TLDefaultColorTheme, TLDefaultColorTheme,
TLDrawShapeSegment, TLDrawShapeSegment,
TLHighlightShape, TLHighlightShape,
@ -116,8 +117,8 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
return <path d={strokePath} /> return <path d={strokePath} />
} }
override toSvg(shape: TLHighlightShape) { override toSvg(shape: TLHighlightShape, ctx: SvgExportContext) {
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
return highlighterToSvg(getStrokeWidth(shape), shape, OVERLAY_OPACITY, theme) return highlighterToSvg(getStrokeWidth(shape), shape, OVERLAY_OPACITY, theme)
} }

View file

@ -4,6 +4,7 @@ import {
Polyline2d, Polyline2d,
SVGContainer, SVGContainer,
ShapeUtil, ShapeUtil,
SvgExportContext,
TLHandle, TLHandle,
TLLineShape, TLLineShape,
TLOnHandleDragHandler, TLOnHandleDragHandler,
@ -306,8 +307,8 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
return <path d={path} /> return <path d={path} />
} }
override toSvg(shape: TLLineShape) { override toSvg(shape: TLLineShape, ctx: SvgExportContext) {
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
const color = theme[shape.props.color].solid const color = theme[shape.props.color].solid
const spline = getGeometryForLineShape(shape) const spline = getGeometryForLineShape(shape)
const strokeWidth = STROKE_SIZES[shape.props.size] const strokeWidth = STROKE_SIZES[shape.props.size]

View file

@ -112,7 +112,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
override toSvg(shape: TLNoteShape, ctx: SvgExportContext) { override toSvg(shape: TLNoteShape, ctx: SvgExportContext) {
ctx.addExportDef(getFontDefForExport(shape.props.font)) ctx.addExportDef(getFontDefForExport(shape.props.font))
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
const bounds = this.editor.getShapeGeometry(shape).bounds const bounds = this.editor.getShapeGeometry(shape).bounds
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g') const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')

View file

@ -150,7 +150,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
override toSvg(shape: TLTextShape, ctx: SvgExportContext) { override toSvg(shape: TLTextShape, ctx: SvgExportContext) {
ctx.addExportDef(getFontDefForExport(shape.props.font)) ctx.addExportDef(getFontDefForExport(shape.props.font))
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
const bounds = this.editor.getShapeGeometry(shape).bounds const bounds = this.editor.getShapeGeometry(shape).bounds
const text = shape.props.text const text = shape.props.text

View file

@ -53,8 +53,8 @@ export class ZoomBrushing extends StateNode {
this.editor.zoomIn(point, { duration: 220 }) this.editor.zoomIn(point, { duration: 220 })
} }
} else { } else {
const zoomLevel = this.editor.inputs.altKey ? this.editor.getZoomLevel() / 2 : undefined const targetZoom = this.editor.inputs.altKey ? this.editor.getZoomLevel() / 2 : undefined
this.editor.zoomToBounds(zoomBrush, zoomLevel, { duration: 220 }) this.editor.zoomToBounds(zoomBrush, { targetZoom, duration: 220 })
} }
this.parent.transition('idle', this.info) this.parent.transition('idle', this.info)

View file

@ -91,7 +91,8 @@ export async function getSvgAsImage(
}) })
} }
async function getSvgAsString(svg: SVGElement) { /** @public */
export async function getSvgAsString(svg: SVGElement) {
const clone = svg.cloneNode(true) as SVGGraphicsElement const clone = svg.cloneNode(true) as SVGGraphicsElement
svg.setAttribute('width', +svg.getAttribute('width')! + '') svg.setAttribute('width', +svg.getAttribute('width')! + '')

View file

@ -591,7 +591,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
const bounds = editor.getCurrentPageBounds() const bounds = editor.getCurrentPageBounds()
if (bounds) { if (bounds) {
editor.zoomToBounds(bounds, 1) editor.zoomToBounds(bounds, { targetZoom: 1 })
} }
}) })
} }

View file

@ -296,7 +296,7 @@ export async function parseAndLoadDocument(
const bounds = editor.getCurrentPageBounds() const bounds = editor.getCurrentPageBounds()
if (bounds) { if (bounds) {
editor.zoomToBounds(bounds, 1) editor.zoomToBounds(bounds, { targetZoom: 1 })
} }
editor.updateInstanceState({ isFocused }) editor.updateInstanceState({ isFocused })
}) })