Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tldraw/tldraw/llms.txt
Use this file to discover all available pages before exploring further.
ImageShapeUtil handles image shapes with support for cropping, circular crops, flipping, and animated images.
Type signature
class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape>
Features
- Image rendering from assets
- Rectangular and circular cropping
- Horizontal and vertical flipping
- Animated image support (GIF, APNG)
- Transparency-aware hit testing
- Alt text for accessibility
- Optional URL linking
Default props
getDefaultProps(): TLImageShape['props'] {
return {
w: 100,
h: 100,
assetId: null,
playing: true,
url: '',
crop: null,
flipX: false,
flipY: false,
altText: '',
}
}
Properties
Width of the image shape.
Height of the image shape.
ID of the associated image asset.
crop
TLShapeCrop | null
default:"null"
Crop settings defining the visible portion:{
topLeft: { x: number, y: number }, // 0-1 normalized
bottomRight: { x: number, y: number }, // 0-1 normalized
isCircle?: boolean
}
Whether to flip the image horizontally.
Whether to flip the image vertically.
Whether animated images should play.
Optional URL for hyperlinking.
Alternative text for accessibility.
Geometry
Image shapes have different geometry based on crop and transparency:
Circular crop
new Ellipse2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true,
})
Transparent PNG/WebP/GIF (alpha-aware)
new ImageRectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true,
alphaDataGetter: () => getAlphaData(src),
crop: shape.props.crop,
flipX: shape.props.flipX,
flipY: shape.props.flipY,
})
Standard rectangle
new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true,
})
Methods
isAspectRatioLocked()
isAspectRatioLocked() {
return true
}
Image aspect ratio is locked to prevent distortion.
canCrop()
canCrop() {
return true
}
Images can be cropped.
isExportBoundsContainer()
isExportBoundsContainer(): boolean {
return true
}
When exporting, images act as bounds containers to avoid extra padding.
getAriaDescriptor()
getAriaDescriptor(shape: TLImageShape) {
return shape.props.altText
}
Returns alt text for accessibility.
onResize()
Handles resizing with flip and crop adjustments:
onResize(shape: TLImageShape, info: TLResizeInfo<TLImageShape>) {
let resized: TLImageShape = resizeBox(shape, info)
const { flipX, flipY } = info.initialShape.props
const { scaleX, scaleY, mode } = info
// Update flip states
resized = {
...resized,
props: {
...resized.props,
flipX: scaleX < 0 !== flipX,
flipY: scaleY < 0 !== flipY,
},
}
if (!shape.props.crop) return resized
// Adjust crop when flipping
const flipCropHorizontally = /* ... */
const flipCropVertically = /* ... */
const { topLeft, bottomRight } = shape.props.crop
resized.props.crop = {
topLeft: {
x: flipCropHorizontally ? 1 - bottomRight.x : topLeft.x,
y: flipCropVertically ? 1 - bottomRight.y : topLeft.y,
},
bottomRight: {
x: flipCropHorizontally ? 1 - topLeft.x : bottomRight.x,
y: flipCropVertically ? 1 - topLeft.y : bottomRight.y,
},
isCircle: shape.props.crop.isCircle,
}
return resized
}
onDoubleClickEdge()
When cropping, double-clicking an edge resets crop to full image:
onDoubleClickEdge(shape: TLImageShape) {
if (this.editor.getCroppingShapeId() !== shape.id) return
const crop = shape.props.crop || {
topLeft: { x: 0, y: 0 },
bottomRight: { x: 1, y: 1 },
}
const { w, h } = getUncroppedSize(shape.props, crop)
const pointDelta = new Vec(crop.topLeft.x * w, crop.topLeft.y * h).rot(shape.rotation)
return {
id: shape.id,
type: shape.type,
x: shape.x - pointDelta.x,
y: shape.y - pointDelta.y,
props: {
crop: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } },
w,
h,
},
}
}
Indicator
Hides indicator when cropping:
useLegacyIndicator() {
return false
}
getIndicatorPath(shape: TLImageShape): Path2D | undefined {
if (this.editor.getCroppingShapeId() === shape.id) return undefined
const path = new Path2D()
if (shape.props.crop?.isCircle) {
const cx = shape.props.w / 2
const cy = shape.props.h / 2
path.ellipse(cx, cy, cx, cy, 0, 0, Math.PI * 2)
} else {
path.rect(0, 0, shape.props.w, shape.props.h)
}
return path
}
Export
toSvg()
Exports image as embedded data URL in SVG:
async toSvg(shape: TLImageShape, ctx: SvgExportContext) {
const props = shape.props
if (!props.assetId) return null
const asset = this.editor.getAsset(props.assetId)
if (!asset) return null
const { w } = getUncroppedSize(shape.props, props.crop)
let src = await ctx.resolveAssetUrl(asset.id, w)
// Convert to data URL if needed
if (src.startsWith('blob:') || src.startsWith('http')) {
src = await getDataURIFromURL(src)
}
// Get first frame for animated images
if (getIsAnimated(this.editor, asset.id)) {
const { promise } = getFirstFrameOfAnimatedImage(src)
src = await promise
}
return <SvgImage shape={shape} src={src} />
}
Example: Create image shapes
// Create image from asset
const assetId = await editor.createImageAsset(file)
editor.createShape({
type: 'image',
props: {
assetId,
w: 400,
h: 300,
altText: 'A beautiful landscape',
}
})
// Create cropped image
editor.createShape({
type: 'image',
props: {
assetId,
w: 200,
h: 200,
crop: {
topLeft: { x: 0.25, y: 0.25 },
bottomRight: { x: 0.75, y: 0.75 },
},
}
})
// Create circular image
editor.createShape({
type: 'image',
props: {
assetId,
w: 200,
h: 200,
crop: {
topLeft: { x: 0, y: 0 },
bottomRight: { x: 1, y: 1 },
isCircle: true,
},
}
})
Example: Flip and crop
// Flip image horizontally
editor.updateShape({
id: imageShape.id,
type: 'image',
props: { flipX: true },
})
// Start cropping
editor.setCroppingShape(imageShape.id)
// Reset crop
editor.updateShape({
id: imageShape.id,
type: 'image',
props: {
crop: {
topLeft: { x: 0, y: 0 },
bottomRight: { x: 1, y: 1 },
},
},
})