{ "version": 3, "sources": ["../../../../src/lib/shapes/image/ImageShapeUtil.tsx"], "sourcesContent": ["/* eslint-disable react-hooks/rules-of-hooks */\nimport {\n\tBaseBoxShapeUtil,\n\tHTMLContainer,\n\tTLImageShape,\n\tTLOnDoubleClickHandler,\n\tTLShapePartial,\n\tVec2d,\n\tdeepCopy,\n\timageShapeMigrations,\n\timageShapeProps,\n\ttoDomPrecision,\n\tuseIsCropping,\n\tuseValue,\n} from '@tldraw/editor'\nimport { useEffect, useState } from 'react'\nimport { HyperlinkButton } from '../shared/HyperlinkButton'\nimport { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'\n\nconst loadImage = async (url: string): Promise => {\n\treturn new Promise((resolve, reject) => {\n\t\tconst image = new Image()\n\t\timage.onload = () => resolve(image)\n\t\timage.onerror = () => reject(new Error('Failed to load image'))\n\t\timage.crossOrigin = 'anonymous'\n\t\timage.src = url\n\t})\n}\n\nconst getStateFrame = async (url: string) => {\n\tconst image = await loadImage(url)\n\n\tconst canvas = document.createElement('canvas')\n\tcanvas.width = image.width\n\tcanvas.height = image.height\n\n\tconst ctx = canvas.getContext('2d')\n\tif (!ctx) return\n\n\tctx.drawImage(image, 0, 0)\n\treturn canvas.toDataURL()\n}\n\nasync function getDataURIFromURL(url: string): Promise {\n\tconst response = await fetch(url)\n\tconst blob = await response.blob()\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader()\n\t\treader.onloadend = () => resolve(reader.result as string)\n\t\treader.onerror = reject\n\t\treader.readAsDataURL(blob)\n\t})\n}\n\n/** @public */\nexport class ImageShapeUtil extends BaseBoxShapeUtil {\n\tstatic override type = 'image' as const\n\tstatic override props = imageShapeProps\n\tstatic override migrations = imageShapeMigrations\n\n\toverride isAspectRatioLocked = () => true\n\toverride canCrop = () => true\n\n\toverride getDefaultProps(): TLImageShape['props'] {\n\t\treturn {\n\t\t\tw: 100,\n\t\t\th: 100,\n\t\t\tassetId: null,\n\t\t\tplaying: true,\n\t\t\turl: '',\n\t\t\tcrop: null,\n\t\t}\n\t}\n\n\tcomponent(shape: TLImageShape) {\n\t\tconst containerStyle = getContainerStyle(shape)\n\t\tconst isCropping = useIsCropping(shape.id)\n\t\tconst prefersReducedMotion = usePrefersReducedMotion()\n\t\tconst [staticFrameSrc, setStaticFrameSrc] = useState('')\n\n\t\tconst asset = shape.props.assetId ? this.editor.getAsset(shape.props.assetId) : undefined\n\n\t\tif (asset?.type === 'bookmark') {\n\t\t\tthrow Error(\"Bookmark assets can't be rendered as images\")\n\t\t}\n\n\t\tconst isSelected = useValue(\n\t\t\t'onlySelectedShape',\n\t\t\t() => shape.id === this.editor.onlySelectedShape?.id,\n\t\t\t[this.editor]\n\t\t)\n\n\t\tconst showCropPreview =\n\t\t\tisSelected &&\n\t\t\tisCropping &&\n\t\t\tthis.editor.isInAny('select.crop', 'select.cropping', 'select.pointing_crop_handle')\n\n\t\t// We only want to reduce motion for mimeTypes that have motion\n\t\tconst reduceMotion =\n\t\t\tprefersReducedMotion &&\n\t\t\t(asset?.props.mimeType?.includes('video') || asset?.props.mimeType?.includes('gif'))\n\n\t\tuseEffect(() => {\n\t\t\tif (asset?.props.src && 'mimeType' in asset.props && asset?.props.mimeType === 'image/gif') {\n\t\t\t\tlet cancelled = false\n\t\t\t\tconst run = async () => {\n\t\t\t\t\tconst newStaticFrame = await getStateFrame(asset.props.src!)\n\t\t\t\t\tif (cancelled) return\n\t\t\t\t\tif (newStaticFrame) {\n\t\t\t\t\t\tsetStaticFrameSrc(newStaticFrame)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trun()\n\n\t\t\t\treturn () => {\n\t\t\t\t\tcancelled = true\n\t\t\t\t}\n\t\t\t}\n\t\t}, [prefersReducedMotion, asset?.props])\n\n\t\treturn (\n\t\t\t<>\n\t\t\t\t{asset?.props.src && showCropPreview && (\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t)}\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t{asset?.props.src ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t{asset?.props.isAnimated && !shape.props.playing && (\n\t\t\t\t\t\t\t
GIF
\n\t\t\t\t\t\t)}\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t\t{'url' in shape.props && shape.props.url && (\n\t\t\t\t\t\n\t\t\t\t)}\n\t\t\t\n\t\t)\n\t}\n\n\tindicator(shape: TLImageShape) {\n\t\tconst isCropping = useIsCropping(shape.id)\n\t\tif (isCropping) {\n\t\t\treturn null\n\t\t}\n\t\treturn \n\t}\n\n\toverride async toSvg(shape: TLImageShape) {\n\t\tconst g = document.createElementNS('http://www.w3.org/2000/svg', 'g')\n\t\tconst asset = shape.props.assetId ? this.editor.getAsset(shape.props.assetId) : null\n\n\t\tlet src = asset?.props.src || ''\n\t\tif (src && src.startsWith('http')) {\n\t\t\t// If it's a remote image, we need to fetch it and convert it to a data URI\n\t\t\tsrc = (await getDataURIFromURL(src)) || ''\n\t\t}\n\n\t\tconst image = document.createElementNS('http://www.w3.org/2000/svg', 'image')\n\t\timage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', src)\n\t\tconst containerStyle = getContainerStyle(shape)\n\t\tconst crop = shape.props.crop\n\t\tif (containerStyle.transform && crop) {\n\t\t\tconst { transform, width, height } = containerStyle\n\t\t\tconst points = [\n\t\t\t\tnew Vec2d(crop.topLeft.x * width, crop.topLeft.y * height),\n\t\t\t\tnew Vec2d(crop.bottomRight.x * width, crop.topLeft.y * height),\n\t\t\t\tnew Vec2d(crop.bottomRight.x * width, crop.bottomRight.y * height),\n\t\t\t\tnew Vec2d(crop.topLeft.x * width, crop.bottomRight.y * height),\n\t\t\t]\n\t\t\tconst innerElement = document.createElementNS('http://www.w3.org/2000/svg', 'g')\n\t\t\tinnerElement.style.clipPath = `polygon(${points.map((p) => `${p.x}px ${p.y}px`).join(',')})`\n\t\t\timage.setAttribute('width', width.toString())\n\t\t\timage.setAttribute('height', height.toString())\n\t\t\timage.style.transform = transform\n\t\t\tinnerElement.appendChild(image)\n\t\t\tg.appendChild(innerElement)\n\t\t} else {\n\t\t\timage.setAttribute('width', shape.props.w.toString())\n\t\t\timage.setAttribute('height', shape.props.h.toString())\n\t\t\tg.appendChild(image)\n\t\t}\n\n\t\treturn g\n\t}\n\n\toverride onDoubleClick = (shape: TLImageShape) => {\n\t\tconst asset = shape.props.assetId ? this.editor.getAsset(shape.props.assetId) : undefined\n\n\t\tif (!asset) return\n\n\t\tconst canPlay =\n\t\t\tasset.props.src && 'mimeType' in asset.props && asset.props.mimeType === 'image/gif'\n\n\t\tif (!canPlay) return\n\n\t\tthis.editor.updateShapes([\n\t\t\t{\n\t\t\t\ttype: 'image',\n\t\t\t\tid: shape.id,\n\t\t\t\tprops: {\n\t\t\t\t\tplaying: !shape.props.playing,\n\t\t\t\t},\n\t\t\t},\n\t\t])\n\t}\n\n\toverride onDoubleClickEdge: TLOnDoubleClickHandler = (shape) => {\n\t\tconst props = shape.props\n\t\tif (!props) return\n\n\t\tif (this.editor.croppingShapeId !== shape.id) {\n\t\t\treturn\n\t\t}\n\n\t\tconst crop = deepCopy(props.crop) || {\n\t\t\ttopLeft: { x: 0, y: 0 },\n\t\t\tbottomRight: { x: 1, y: 1 },\n\t\t}\n\n\t\t// The true asset dimensions\n\t\tconst w = (1 / (crop.bottomRight.x - crop.topLeft.x)) * shape.props.w\n\t\tconst h = (1 / (crop.bottomRight.y - crop.topLeft.y)) * shape.props.h\n\n\t\tconst pointDelta = new Vec2d(crop.topLeft.x * w, crop.topLeft.y * h).rot(shape.rotation)\n\n\t\tconst partial: TLShapePartial = {\n\t\t\tid: shape.id,\n\t\t\ttype: shape.type,\n\t\t\tx: shape.x - pointDelta.x,\n\t\t\ty: shape.y - pointDelta.y,\n\t\t\tprops: {\n\t\t\t\tcrop: {\n\t\t\t\t\ttopLeft: { x: 0, y: 0 },\n\t\t\t\t\tbottomRight: { x: 1, y: 1 },\n\t\t\t\t},\n\t\t\t\tw,\n\t\t\t\th,\n\t\t\t},\n\t\t}\n\n\t\tthis.editor.updateShapes([partial])\n\t}\n}\n\n/**\n * When an image is cropped we need to translate the image to show the portion withing the cropped\n * area. We do this by translating the image by the negative of the top left corner of the crop\n * area.\n *\n * @param shape - Shape The image shape for which to get the container style\n * @returns - Styles to apply to the image container\n */\nfunction getContainerStyle(shape: TLImageShape) {\n\tconst crop = shape.props.crop\n\tconst topLeft = crop?.topLeft\n\tif (!topLeft) {\n\t\treturn {\n\t\t\twidth: shape.props.w,\n\t\t\theight: shape.props.h,\n\t\t}\n\t}\n\n\tconst w = (1 / (crop.bottomRight.x - crop.topLeft.x)) * shape.props.w\n\tconst h = (1 / (crop.bottomRight.y - crop.topLeft.y)) * shape.props.h\n\n\tconst offsetX = -topLeft.x * w\n\tconst offsetY = -topLeft.y * h\n\treturn {\n\t\ttransform: `translate(${offsetX}px, ${offsetY}px)`,\n\t\twidth: w,\n\t\theight: h,\n\t}\n}\n"], "mappings": "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", "names": [] }