@tldraw/editor 3.8.0-canary.46ed9df28e1c → 3.8.0-canary.4703b6039d91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +144 -12
- package/dist-cjs/index.js +5 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -4
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +3 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +62 -5
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/SvgExportContext.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgAsImage.js +83 -0
- package/dist-cjs/lib/exports/getSvgAsImage.js.map +7 -0
- package/dist-cjs/lib/exports/getSvgJsx.js +16 -3
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useLocalStore.js +1 -1
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/utils/browserCanvasMaxSize.js +75 -0
- package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +7 -0
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +144 -12
- package/dist-esm/index.mjs +5 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -4
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +3 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +62 -5
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/types/SvgExportContext.mjs.map +2 -2
- package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgAsImage.mjs +63 -0
- package/dist-esm/lib/exports/getSvgAsImage.mjs.map +7 -0
- package/dist-esm/lib/exports/getSvgJsx.mjs +16 -3
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useLocalStore.mjs +1 -1
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +45 -0
- package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +9 -7
- package/src/index.ts +5 -0
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -4
- package/src/lib/config/createTLStore.ts +3 -1
- package/src/lib/editor/Editor.ts +90 -12
- package/src/lib/editor/shapes/ShapeUtil.ts +30 -1
- package/src/lib/editor/types/SvgExportContext.tsx +21 -0
- package/src/lib/editor/types/misc-types.ts +55 -2
- package/src/lib/exports/exportToSvg.tsx +2 -2
- package/src/lib/exports/getSvgAsImage.ts +92 -0
- package/src/lib/exports/getSvgJsx.tsx +17 -2
- package/src/lib/hooks/useLocalStore.ts +1 -1
- package/src/lib/utils/browserCanvasMaxSize.ts +65 -0
- package/src/version.ts +3 -3
|
@@ -485,10 +485,7 @@ function DebugSvgCopy({ id, mode }: { id: TLShapeId; mode: 'img' | 'iframe' }) {
|
|
|
485
485
|
if (!bounds) return
|
|
486
486
|
bounds = bounds.clone().expandBy(padding)
|
|
487
487
|
|
|
488
|
-
const result = await editor.getSvgString([id], {
|
|
489
|
-
padding,
|
|
490
|
-
background: editor.getInstanceState().exportBackground,
|
|
491
|
-
})
|
|
488
|
+
const result = await editor.getSvgString([id], { padding })
|
|
492
489
|
|
|
493
490
|
if (latest !== renderId || !result) return
|
|
494
491
|
|
|
@@ -61,7 +61,9 @@ const defaultAssetResolve: NonNullable<TLAssetStore['resolve']> = (asset) => ass
|
|
|
61
61
|
|
|
62
62
|
/** @public */
|
|
63
63
|
export const inlineBase64AssetStore: TLAssetStore = {
|
|
64
|
-
upload: (_, file) =>
|
|
64
|
+
upload: async (_, file) => {
|
|
65
|
+
return { src: await FileHelpers.blobToDataUrl(file) }
|
|
66
|
+
},
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
ZOOM_TO_FIT_PADDING,
|
|
105
105
|
} from '../constants'
|
|
106
106
|
import { exportToSvg } from '../exports/exportToSvg'
|
|
107
|
+
import { getSvgAsImage } from '../exports/getSvgAsImage'
|
|
107
108
|
import { tlenv } from '../globals/environment'
|
|
108
109
|
import { tlmenus } from '../globals/menus'
|
|
109
110
|
import { tltime } from '../globals/time'
|
|
@@ -162,6 +163,7 @@ import {
|
|
|
162
163
|
TLCameraMoveOptions,
|
|
163
164
|
TLCameraOptions,
|
|
164
165
|
TLImageExportOptions,
|
|
166
|
+
TLSvgExportOptions,
|
|
165
167
|
} from './types/misc-types'
|
|
166
168
|
import { TLResizeHandle } from './types/selection-types'
|
|
167
169
|
|
|
@@ -927,6 +929,21 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
927
929
|
return shapeUtil
|
|
928
930
|
}
|
|
929
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Returns true if the editor has a shape util for the given shape / shape type.
|
|
934
|
+
*
|
|
935
|
+
* @param shape - A shape, shape partial, or shape type.
|
|
936
|
+
*/
|
|
937
|
+
hasShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): boolean
|
|
938
|
+
hasShapeUtil<S extends TLUnknownShape>(type: S['type']): boolean
|
|
939
|
+
hasShapeUtil<T extends ShapeUtil>(
|
|
940
|
+
type: T extends ShapeUtil<infer R> ? R['type'] : string
|
|
941
|
+
): boolean
|
|
942
|
+
hasShapeUtil(arg: string | { type: string }): boolean {
|
|
943
|
+
const type = typeof arg === 'string' ? arg : arg.type
|
|
944
|
+
return hasOwnProperty(this.shapeUtils, type)
|
|
945
|
+
}
|
|
946
|
+
|
|
930
947
|
/* ------------------- Binding Utils ------------------ */
|
|
931
948
|
/**
|
|
932
949
|
* A map of shape utility classes (TLShapeUtils) by shape type.
|
|
@@ -1458,10 +1475,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1458
1475
|
if (partial.isChangingStyle !== undefined) {
|
|
1459
1476
|
clearTimeout(this._isChangingStyleTimeout)
|
|
1460
1477
|
if (partial.isChangingStyle === true) {
|
|
1461
|
-
// If we've set to true, set a new reset timeout to change the value back to false after
|
|
1478
|
+
// If we've set to true, set a new reset timeout to change the value back to false after 1 seconds
|
|
1462
1479
|
this._isChangingStyleTimeout = this.timers.setTimeout(() => {
|
|
1463
1480
|
this._updateInstanceState({ isChangingStyle: false }, { history: 'ignore' })
|
|
1464
|
-
},
|
|
1481
|
+
}, 1000)
|
|
1465
1482
|
}
|
|
1466
1483
|
}
|
|
1467
1484
|
|
|
@@ -4145,20 +4162,24 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4145
4162
|
context: {
|
|
4146
4163
|
screenScale?: number
|
|
4147
4164
|
shouldResolveToOriginal?: boolean
|
|
4165
|
+
dpr?: number
|
|
4148
4166
|
}
|
|
4149
4167
|
): Promise<string | null> {
|
|
4150
4168
|
if (!assetId) return null
|
|
4151
4169
|
const asset = this.getAsset(assetId)
|
|
4152
4170
|
if (!asset) return null
|
|
4153
4171
|
|
|
4154
|
-
const {
|
|
4172
|
+
const {
|
|
4173
|
+
screenScale = 1,
|
|
4174
|
+
shouldResolveToOriginal = false,
|
|
4175
|
+
dpr = this.getInstanceState().devicePixelRatio,
|
|
4176
|
+
} = context
|
|
4155
4177
|
|
|
4156
4178
|
// We only look at the zoom level at powers of 2.
|
|
4157
4179
|
const zoomStepFunction = (zoom: number) => Math.pow(2, Math.ceil(Math.log2(zoom)))
|
|
4158
|
-
const steppedScreenScale =
|
|
4180
|
+
const steppedScreenScale = zoomStepFunction(screenScale)
|
|
4159
4181
|
const networkEffectiveType: string | null =
|
|
4160
4182
|
'connection' in navigator ? (navigator as any).connection.effectiveType : null
|
|
4161
|
-
const dpr = this.getInstanceState().devicePixelRatio
|
|
4162
4183
|
|
|
4163
4184
|
return await this.store.props.assets.resolve(asset, {
|
|
4164
4185
|
screenScale: screenScale || 1,
|
|
@@ -4172,7 +4193,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4172
4193
|
* Upload an asset to the store's asset service, returning a URL that can be used to resolve the
|
|
4173
4194
|
* asset.
|
|
4174
4195
|
*/
|
|
4175
|
-
async uploadAsset(
|
|
4196
|
+
async uploadAsset(
|
|
4197
|
+
asset: TLAsset,
|
|
4198
|
+
file: File,
|
|
4199
|
+
abortSignal?: AbortSignal
|
|
4200
|
+
): Promise<{ src: string; meta?: JsonObject }> {
|
|
4176
4201
|
return await this.store.props.assets.upload(asset, file, abortSignal)
|
|
4177
4202
|
}
|
|
4178
4203
|
|
|
@@ -8564,11 +8589,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8564
8589
|
*
|
|
8565
8590
|
* @public
|
|
8566
8591
|
*/
|
|
8567
|
-
async getSvgElement(shapes: TLShapeId[] | TLShape[], opts:
|
|
8592
|
+
async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
|
|
8568
8593
|
const ids =
|
|
8569
|
-
|
|
8570
|
-
? (
|
|
8571
|
-
:
|
|
8594
|
+
shapes.length === 0
|
|
8595
|
+
? this.getCurrentPageShapeIdsSorted()
|
|
8596
|
+
: typeof shapes[0] === 'string'
|
|
8597
|
+
? (shapes as TLShapeId[])
|
|
8598
|
+
: (shapes as TLShape[]).map((s) => s.id)
|
|
8572
8599
|
|
|
8573
8600
|
if (ids.length === 0) return undefined
|
|
8574
8601
|
|
|
@@ -8585,7 +8612,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8585
8612
|
*
|
|
8586
8613
|
* @public
|
|
8587
8614
|
*/
|
|
8588
|
-
async getSvgString(shapes: TLShapeId[] | TLShape[], opts:
|
|
8615
|
+
async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
|
|
8589
8616
|
const result = await this.getSvgElement(shapes, opts)
|
|
8590
8617
|
if (!result) return undefined
|
|
8591
8618
|
|
|
@@ -8598,12 +8625,63 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8598
8625
|
}
|
|
8599
8626
|
|
|
8600
8627
|
/** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */
|
|
8601
|
-
async getSvg(shapes: TLShapeId[] | TLShape[], opts:
|
|
8628
|
+
async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
|
|
8602
8629
|
const result = await this.getSvgElement(shapes, opts)
|
|
8603
8630
|
if (!result) return undefined
|
|
8604
8631
|
return result.svg
|
|
8605
8632
|
}
|
|
8606
8633
|
|
|
8634
|
+
/**
|
|
8635
|
+
* Get an exported image of the given shapes.
|
|
8636
|
+
*
|
|
8637
|
+
* @param shapes - The shapes (or shape ids) to export.
|
|
8638
|
+
* @param opts - Options for the export.
|
|
8639
|
+
*
|
|
8640
|
+
* @returns A blob of the image.
|
|
8641
|
+
* @public
|
|
8642
|
+
*/
|
|
8643
|
+
async toImage(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
|
|
8644
|
+
const withDefaults = {
|
|
8645
|
+
format: 'png',
|
|
8646
|
+
scale: 1,
|
|
8647
|
+
pixelRatio: opts.format === 'svg' ? undefined : 2,
|
|
8648
|
+
...opts,
|
|
8649
|
+
} satisfies TLImageExportOptions
|
|
8650
|
+
const result = await this.getSvgString(shapes, withDefaults)
|
|
8651
|
+
if (!result) throw new Error('Could not create SVG')
|
|
8652
|
+
|
|
8653
|
+
switch (withDefaults.format) {
|
|
8654
|
+
case 'svg':
|
|
8655
|
+
return {
|
|
8656
|
+
blob: new Blob([result.svg], { type: 'text/plain' }),
|
|
8657
|
+
width: result.width,
|
|
8658
|
+
height: result.height,
|
|
8659
|
+
}
|
|
8660
|
+
case 'jpeg':
|
|
8661
|
+
case 'png':
|
|
8662
|
+
case 'webp': {
|
|
8663
|
+
const blob = await getSvgAsImage(result.svg, {
|
|
8664
|
+
type: withDefaults.format,
|
|
8665
|
+
quality: withDefaults.quality,
|
|
8666
|
+
pixelRatio: withDefaults.pixelRatio,
|
|
8667
|
+
width: result.width,
|
|
8668
|
+
height: result.height,
|
|
8669
|
+
})
|
|
8670
|
+
if (!blob) {
|
|
8671
|
+
throw new Error('Could not construct image.')
|
|
8672
|
+
}
|
|
8673
|
+
return {
|
|
8674
|
+
blob,
|
|
8675
|
+
width: result.width,
|
|
8676
|
+
height: result.height,
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8679
|
+
default: {
|
|
8680
|
+
exhaustiveSwitchError(withDefaults.format)
|
|
8681
|
+
}
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
|
|
8607
8685
|
/* --------------------- Events --------------------- */
|
|
8608
8686
|
|
|
8609
8687
|
/**
|
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
TLHandle,
|
|
6
6
|
TLPropsMigrations,
|
|
7
7
|
TLShape,
|
|
8
|
+
TLShapeCrop,
|
|
8
9
|
TLShapePartial,
|
|
9
10
|
TLUnknownShape,
|
|
10
11
|
} from '@tldraw/tlschema'
|
|
11
12
|
import { ReactElement } from 'react'
|
|
12
|
-
import { Box } from '../../primitives/Box'
|
|
13
|
+
import { Box, SelectionHandle } from '../../primitives/Box'
|
|
13
14
|
import { Vec } from '../../primitives/Vec'
|
|
14
15
|
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
|
15
16
|
import type { Editor } from '../Editor'
|
|
@@ -419,6 +420,19 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|
|
419
420
|
*/
|
|
420
421
|
onBeforeUpdate?(prev: Shape, next: Shape): Shape | void
|
|
421
422
|
|
|
423
|
+
/**
|
|
424
|
+
* A callback called when a shape changes from a crop.
|
|
425
|
+
*
|
|
426
|
+
* @param shape - The shape at the start of the crop.
|
|
427
|
+
* @param info - Info about the crop.
|
|
428
|
+
* @returns A change to apply to the shape, or void.
|
|
429
|
+
* @public
|
|
430
|
+
*/
|
|
431
|
+
onCrop?(
|
|
432
|
+
shape: Shape,
|
|
433
|
+
info: TLCropInfo<Shape>
|
|
434
|
+
): Omit<TLShapePartial<Shape>, 'id' | 'type'> | undefined | void
|
|
435
|
+
|
|
422
436
|
/**
|
|
423
437
|
* A callback called when some other shapes are dragged over this one.
|
|
424
438
|
*
|
|
@@ -616,6 +630,21 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|
|
616
630
|
onEditEnd?(shape: Shape): void
|
|
617
631
|
}
|
|
618
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Info about a crop.
|
|
635
|
+
* @param handle - The handle being dragged.
|
|
636
|
+
* @param change - The distance the handle is moved.
|
|
637
|
+
* @param initialShape - The shape at the start of the resize.
|
|
638
|
+
* @public
|
|
639
|
+
*/
|
|
640
|
+
export interface TLCropInfo<T extends TLShape> {
|
|
641
|
+
handle: SelectionHandle
|
|
642
|
+
change: Vec
|
|
643
|
+
crop: TLShapeCrop
|
|
644
|
+
uncroppedSize: { w: number; h: number }
|
|
645
|
+
initialShape: T
|
|
646
|
+
}
|
|
647
|
+
|
|
619
648
|
/**
|
|
620
649
|
* The type of resize.
|
|
621
650
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TLAssetId } from '@tldraw/tlschema'
|
|
1
2
|
import { promiseWithResolve } from '@tldraw/utils'
|
|
2
3
|
import { ReactElement, ReactNode, createContext, useContext, useEffect, useState } from 'react'
|
|
3
4
|
import { ContainerProvider } from '../../hooks/useContainer'
|
|
@@ -29,10 +30,30 @@ export interface SvgExportContext {
|
|
|
29
30
|
*/
|
|
30
31
|
waitUntil(promise: Promise<void>): void
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Resolve an asset URL in the context of this export. Supply the asset ID and the width in
|
|
35
|
+
* shape-pixels it'll be displayed at, and this will resolve the asset according to the export
|
|
36
|
+
* options.
|
|
37
|
+
*/
|
|
38
|
+
resolveAssetUrl(assetId: TLAssetId, width: number): Promise<string | null>
|
|
39
|
+
|
|
32
40
|
/**
|
|
33
41
|
* Whether the export should be in dark mode.
|
|
34
42
|
*/
|
|
35
43
|
readonly isDarkMode: boolean
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The scale of the export - how much CSS pixels will be scaled up/down by.
|
|
47
|
+
*/
|
|
48
|
+
readonly scale: number
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Use this value to optionally downscale images in the export. If we're exporting directly to
|
|
52
|
+
* an SVG, this will usually be null, and you shouldn't downscale images. If the export is to a
|
|
53
|
+
* raster format like PNG, this will be the number of raster pixels in the resulting bitmap per
|
|
54
|
+
* CSS pixel in the resulting SVG.
|
|
55
|
+
*/
|
|
56
|
+
readonly pixelRatio: number | null
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
const Context = createContext<SvgExportContext | null>(null)
|
|
@@ -8,17 +8,70 @@ export type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T,
|
|
|
8
8
|
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
|
9
9
|
|
|
10
10
|
/** @public */
|
|
11
|
-
export
|
|
11
|
+
export type TLExportType = 'svg' | 'png' | 'jpeg' | 'webp'
|
|
12
|
+
|
|
13
|
+
/** @public */
|
|
14
|
+
export interface TLSvgExportOptions {
|
|
15
|
+
/**
|
|
16
|
+
* The bounding box, in page coordinates, of the area being exported.
|
|
17
|
+
*/
|
|
12
18
|
bounds?: Box
|
|
19
|
+
/**
|
|
20
|
+
* The logical scale of the export. This scales the resulting size of the SVG being generated.
|
|
21
|
+
*/
|
|
13
22
|
scale?: number
|
|
14
|
-
|
|
23
|
+
/**
|
|
24
|
+
* When exporting an SVG, the expected pixel ratio of the export will be passed in to
|
|
25
|
+
* {@link @tldraw/tlschema#TLAssetStore.resolve} as the `dpr` property, so that assets can be
|
|
26
|
+
* downscaled to the appropriate resolution.
|
|
27
|
+
*
|
|
28
|
+
* When exporting to a bitmap image format, the size of the resulting image will be multiplied
|
|
29
|
+
* by this number.
|
|
30
|
+
*
|
|
31
|
+
* For SVG exports, this defaults to undefined - which means we'll request original-quality
|
|
32
|
+
* assets. For bitmap exports, this defaults to 2.
|
|
33
|
+
*/
|
|
15
34
|
pixelRatio?: number
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Should the background color be included in the export? If false, the generated image will be
|
|
38
|
+
* transparent (if exporting to a format that supports transparency).
|
|
39
|
+
*/
|
|
16
40
|
background?: boolean
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How much padding to include around the bounds of exports? Defaults to 32px.
|
|
44
|
+
*/
|
|
17
45
|
padding?: number
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Should the export be rendered in dark mode (true) or light mode (false)? Defaults to the
|
|
49
|
+
* current instance's dark mode setting.
|
|
50
|
+
*/
|
|
18
51
|
darkMode?: boolean
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The
|
|
55
|
+
* {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio | `preserveAspectRatio` }
|
|
56
|
+
* attribute of the SVG element.
|
|
57
|
+
*/
|
|
19
58
|
preserveAspectRatio?: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
|
|
20
59
|
}
|
|
21
60
|
|
|
61
|
+
/** @public */
|
|
62
|
+
export interface TLImageExportOptions extends TLSvgExportOptions {
|
|
63
|
+
/**
|
|
64
|
+
* If the export is being converted to a lossy bitmap format (e.g. jpeg), this is the quality of
|
|
65
|
+
* the export. This is a number between 0 and 1.
|
|
66
|
+
*/
|
|
67
|
+
quality?: number
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The format to export as. Defaults to 'png'.
|
|
71
|
+
*/
|
|
72
|
+
format?: TLExportType
|
|
73
|
+
}
|
|
74
|
+
|
|
22
75
|
/**
|
|
23
76
|
* @public
|
|
24
77
|
* @deprecated use {@link TLImageExportOptions} instead
|
|
@@ -3,7 +3,7 @@ import { assert } from '@tldraw/utils'
|
|
|
3
3
|
import { flushSync } from 'react-dom'
|
|
4
4
|
import { createRoot } from 'react-dom/client'
|
|
5
5
|
import { Editor } from '../editor/Editor'
|
|
6
|
-
import {
|
|
6
|
+
import { TLSvgExportOptions } from '../editor/types/misc-types'
|
|
7
7
|
import { StyleEmbedder } from './StyleEmbedder'
|
|
8
8
|
import { embedMedia } from './embedMedia'
|
|
9
9
|
import { getSvgJsx } from './getSvgJsx'
|
|
@@ -13,7 +13,7 @@ let idCounter = 1
|
|
|
13
13
|
export async function exportToSvg(
|
|
14
14
|
editor: Editor,
|
|
15
15
|
shapeIds: TLShapeId[],
|
|
16
|
-
opts:
|
|
16
|
+
opts: TLSvgExportOptions = {}
|
|
17
17
|
) {
|
|
18
18
|
// when rendering to SVG, we start by creating a JSX representation of the SVG that we can
|
|
19
19
|
// render with react. Hopefully elements will have a `toSvg` method that renders them to SVG,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { FileHelpers, Image, PngHelpers, sleep } from '@tldraw/utils'
|
|
2
|
+
import { tlenv } from '../globals/environment'
|
|
3
|
+
import { clampToBrowserMaxCanvasSize } from '../utils/browserCanvasMaxSize'
|
|
4
|
+
import { debugFlags } from '../utils/debug-flags'
|
|
5
|
+
|
|
6
|
+
/** @public */
|
|
7
|
+
export async function getSvgAsImage(
|
|
8
|
+
svgString: string,
|
|
9
|
+
options: {
|
|
10
|
+
type: 'png' | 'jpeg' | 'webp'
|
|
11
|
+
width: number
|
|
12
|
+
height: number
|
|
13
|
+
quality?: number
|
|
14
|
+
pixelRatio?: number
|
|
15
|
+
}
|
|
16
|
+
) {
|
|
17
|
+
const { type, width, height, quality = 1, pixelRatio = 2 } = options
|
|
18
|
+
|
|
19
|
+
let [clampedWidth, clampedHeight] = await clampToBrowserMaxCanvasSize(
|
|
20
|
+
width * pixelRatio,
|
|
21
|
+
height * pixelRatio
|
|
22
|
+
)
|
|
23
|
+
clampedWidth = Math.floor(clampedWidth)
|
|
24
|
+
clampedHeight = Math.floor(clampedHeight)
|
|
25
|
+
const effectiveScale = clampedWidth / width
|
|
26
|
+
|
|
27
|
+
// usually we would use `URL.createObjectURL` here, but chrome has a bug where `blob:` URLs of
|
|
28
|
+
// SVGs that use <foreignObject> mark the canvas as tainted, where data: ones do not.
|
|
29
|
+
// https://issues.chromium.org/issues/41054640
|
|
30
|
+
const svgUrl = await FileHelpers.blobToDataUrl(new Blob([svgString], { type: 'image/svg+xml' }))
|
|
31
|
+
|
|
32
|
+
const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
|
|
33
|
+
const image = Image()
|
|
34
|
+
image.crossOrigin = 'anonymous'
|
|
35
|
+
|
|
36
|
+
image.onload = async () => {
|
|
37
|
+
// safari will fire `onLoad` before the fonts in the SVG are
|
|
38
|
+
// actually loaded. just waiting around a while is brittle, but
|
|
39
|
+
// there doesn't seem to be any better solution for now :( see
|
|
40
|
+
// https://bugs.webkit.org/show_bug.cgi?id=219770
|
|
41
|
+
if (tlenv.isSafari) {
|
|
42
|
+
await sleep(250)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const canvas = document.createElement('canvas') as HTMLCanvasElement
|
|
46
|
+
const ctx = canvas.getContext('2d')!
|
|
47
|
+
|
|
48
|
+
canvas.width = clampedWidth
|
|
49
|
+
canvas.height = clampedHeight
|
|
50
|
+
|
|
51
|
+
ctx.imageSmoothingEnabled = true
|
|
52
|
+
ctx.imageSmoothingQuality = 'high'
|
|
53
|
+
ctx.drawImage(image, 0, 0, clampedWidth, clampedHeight)
|
|
54
|
+
|
|
55
|
+
URL.revokeObjectURL(svgUrl)
|
|
56
|
+
|
|
57
|
+
resolve(canvas)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
image.onerror = () => {
|
|
61
|
+
resolve(null)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
image.src = svgUrl
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!canvas) return null
|
|
68
|
+
|
|
69
|
+
const blob = await new Promise<Blob | null>((resolve) =>
|
|
70
|
+
canvas.toBlob(
|
|
71
|
+
(blob) => {
|
|
72
|
+
if (!blob || debugFlags.throwToBlob.get()) {
|
|
73
|
+
resolve(null)
|
|
74
|
+
}
|
|
75
|
+
resolve(blob)
|
|
76
|
+
},
|
|
77
|
+
'image/' + type,
|
|
78
|
+
quality
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (!blob) return null
|
|
83
|
+
|
|
84
|
+
if (type === 'png') {
|
|
85
|
+
const view = new DataView(await blob.arrayBuffer())
|
|
86
|
+
return PngHelpers.setPhysChunk(view, effectiveScale, {
|
|
87
|
+
type: 'image/' + type,
|
|
88
|
+
})
|
|
89
|
+
} else {
|
|
90
|
+
return blob
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -41,7 +41,7 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
|
|
|
41
41
|
const {
|
|
42
42
|
scale = 1,
|
|
43
43
|
// should we include the background in the export? or is it transparent?
|
|
44
|
-
background =
|
|
44
|
+
background = editor.getInstanceState().exportBackground,
|
|
45
45
|
padding = editor.options.defaultSvgPadding,
|
|
46
46
|
preserveAspectRatio,
|
|
47
47
|
} = opts
|
|
@@ -102,6 +102,7 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
|
|
|
102
102
|
editor={editor}
|
|
103
103
|
preserveAspectRatio={preserveAspectRatio}
|
|
104
104
|
scale={scale}
|
|
105
|
+
pixelRatio={opts.pixelRatio ?? null}
|
|
105
106
|
bbox={bbox}
|
|
106
107
|
background={background}
|
|
107
108
|
singleFrameShapeId={singleFrameShapeId}
|
|
@@ -121,6 +122,7 @@ function SvgExport({
|
|
|
121
122
|
editor,
|
|
122
123
|
preserveAspectRatio,
|
|
123
124
|
scale,
|
|
125
|
+
pixelRatio,
|
|
124
126
|
bbox,
|
|
125
127
|
background,
|
|
126
128
|
singleFrameShapeId,
|
|
@@ -132,6 +134,7 @@ function SvgExport({
|
|
|
132
134
|
editor: Editor
|
|
133
135
|
preserveAspectRatio?: string
|
|
134
136
|
scale: number
|
|
137
|
+
pixelRatio: number | null
|
|
135
138
|
bbox: Box
|
|
136
139
|
background: boolean
|
|
137
140
|
singleFrameShapeId: TLShapeId | null
|
|
@@ -177,8 +180,20 @@ function SvgExport({
|
|
|
177
180
|
isDarkMode,
|
|
178
181
|
waitUntil,
|
|
179
182
|
addExportDef,
|
|
183
|
+
scale,
|
|
184
|
+
pixelRatio,
|
|
185
|
+
async resolveAssetUrl(assetId, width) {
|
|
186
|
+
const asset = editor.getAsset(assetId)
|
|
187
|
+
if (!asset || (asset.type !== 'image' && asset.type !== 'video')) return null
|
|
188
|
+
|
|
189
|
+
return await editor.resolveAssetUrl(assetId, {
|
|
190
|
+
screenScale: scale * (width / asset.props.w),
|
|
191
|
+
shouldResolveToOriginal: pixelRatio === null,
|
|
192
|
+
dpr: pixelRatio ?? undefined,
|
|
193
|
+
})
|
|
194
|
+
},
|
|
180
195
|
}),
|
|
181
|
-
[isDarkMode, waitUntil, addExportDef]
|
|
196
|
+
[isDarkMode, waitUntil, addExportDef, scale, pixelRatio, editor]
|
|
182
197
|
)
|
|
183
198
|
|
|
184
199
|
const didRenderRef = useRef(false)
|
|
@@ -37,7 +37,7 @@ export function useLocalStore(
|
|
|
37
37
|
const assets: TLAssetStore = {
|
|
38
38
|
upload: async (asset, file) => {
|
|
39
39
|
await client.db.storeAsset(asset.id, file)
|
|
40
|
-
return asset.id
|
|
40
|
+
return { src: asset.id }
|
|
41
41
|
},
|
|
42
42
|
resolve: async (asset) => {
|
|
43
43
|
if (!asset.props.src) return null
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import canvasSize from 'canvas-size'
|
|
2
|
+
|
|
3
|
+
/** @internal */
|
|
4
|
+
export interface CanvasMaxSize {
|
|
5
|
+
maxWidth: number
|
|
6
|
+
maxHeight: number
|
|
7
|
+
maxArea: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let maxSizePromise: Promise<CanvasMaxSize> | null = null
|
|
11
|
+
|
|
12
|
+
function getBrowserCanvasMaxSize() {
|
|
13
|
+
if (!maxSizePromise) {
|
|
14
|
+
maxSizePromise = calculateBrowserCanvasMaxSize()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return maxSizePromise
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function calculateBrowserCanvasMaxSize(): Promise<CanvasMaxSize> {
|
|
21
|
+
const maxWidth = await canvasSize.maxWidth({ usePromise: true })
|
|
22
|
+
const maxHeight = await canvasSize.maxHeight({ usePromise: true })
|
|
23
|
+
const maxArea = await canvasSize.maxArea({ usePromise: true })
|
|
24
|
+
return {
|
|
25
|
+
maxWidth: maxWidth.width,
|
|
26
|
+
maxHeight: maxHeight.height,
|
|
27
|
+
maxArea: maxArea.width * maxArea.height,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// https://github.com/jhildenbiddle/canvas-size?tab=readme-ov-file#test-results
|
|
32
|
+
const MAX_SAFE_CANVAS_DIMENSION = 8192
|
|
33
|
+
const MAX_SAFE_CANVAS_AREA = 4096 * 4096
|
|
34
|
+
|
|
35
|
+
/** @internal */
|
|
36
|
+
export async function clampToBrowserMaxCanvasSize(width: number, height: number) {
|
|
37
|
+
if (
|
|
38
|
+
width <= MAX_SAFE_CANVAS_DIMENSION &&
|
|
39
|
+
height <= MAX_SAFE_CANVAS_DIMENSION &&
|
|
40
|
+
width * height <= MAX_SAFE_CANVAS_AREA
|
|
41
|
+
) {
|
|
42
|
+
return [width, height]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { maxWidth, maxHeight, maxArea } = await getBrowserCanvasMaxSize()
|
|
46
|
+
const aspectRatio = width / height
|
|
47
|
+
|
|
48
|
+
if (width > maxWidth) {
|
|
49
|
+
width = maxWidth
|
|
50
|
+
height = width / aspectRatio
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (height > maxHeight) {
|
|
54
|
+
height = maxHeight
|
|
55
|
+
width = height * aspectRatio
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (width * height > maxArea) {
|
|
59
|
+
const ratio = Math.sqrt(maxArea / (width * height))
|
|
60
|
+
width *= ratio
|
|
61
|
+
height *= ratio
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [width, height]
|
|
65
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '3.8.0-canary.
|
|
4
|
+
export const version = '3.8.0-canary.4703b6039d91'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2024-09-13T14:36:29.063Z',
|
|
7
|
-
minor: '2025-01-
|
|
8
|
-
patch: '2025-01-
|
|
7
|
+
minor: '2025-01-28T10:50:55.780Z',
|
|
8
|
+
patch: '2025-01-28T10:50:55.780Z',
|
|
9
9
|
}
|