diff --git a/README.md b/README.md index 1205030..ac1e1f8 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ And open `localhost:3000` Draw something in the rectangle! Double-clicking the prompt to change it. -Click the small arrow to enter *lens mode*. +Click the small arrow to enter _lens mode_. ## 6. Share! @@ -79,28 +79,23 @@ Sign in on [CodeSandbox](https://codesandbox.io). Click on **Import repository** ![a](https://github.com/tldraw/draw-fast/assets/15892272/dce56531-ca82-473d-b2ef-fe13644c7fb3) - Import the repo by pasting in `https://github.com/tldraw/draw-fast` and clicking **Import**. ![b](https://github.com/tldraw/draw-fast/assets/15892272/000597fe-69e0-43a0-96fb-89ab242c31f3) - ## 2. Setup the environment Click **Next** until you get to the **Set environment variables** screen. ![c](https://github.com/tldraw/draw-fast/assets/15892272/d321b780-c33c-4217-b647-f757182869f3) - On the **Set environment variables** screen, click **Add variable**. ![d](https://github.com/tldraw/draw-fast/assets/15892272/65699754-9a54-4406-a28b-285d94488997) - Name your key `FAL_KEY`. You can get a key from [fal.ai](https://www.fal.ai/dashboard/keys) Instructions on how to do that are [here](https://www.notion.so/Draw-Fast-help-038edf9a982847e19df078854c54c8dd?pvs=21). ![e](https://github.com/tldraw/draw-fast/assets/15892272/4c2a128c-a597-4578-87c4-44e73e29de86) - Click **Save**, then click **Next** until you get to the end of setup. ![f](https://github.com/tldraw/draw-fast/assets/15892272/9467f645-5843-445c-b346-68f2617c1d02) @@ -108,12 +103,11 @@ Click **Save**, then click **Next** until you get to the end of setup. Finally, click **Apply and restart**, and wait about 5 minutes. ![g](https://github.com/tldraw/draw-fast/assets/15892272/b9ba8c65-7b28-4e80-a760-6b1814244c7b) - ## 3. Draw fast Draw something in the rectangle! Double-clicking the prompt to change it. -Click the small arrow to enter *lens mode*. +Click the small arrow to enter _lens mode_. ## 4. Share! diff --git a/package-lock.json b/package-lock.json index 98373b4..c7292e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@fal-ai/serverless-client": "^0.6.0", - "@fal-ai/serverless-proxy": "^0.6.0", + "@fal-ai/serverless-client": "^0.8.2", + "@fal-ai/serverless-proxy": "^0.7.3", "@tldraw/tldraw": "canary", "next": "14.0.3", "react": "^18", @@ -108,14 +108,22 @@ } }, "node_modules/@fal-ai/serverless-client": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@fal-ai/serverless-client/-/serverless-client-0.6.1.tgz", - "integrity": "sha512-MwVR/J9yfZIGUT004Qp1ns6JP55FO8Veg6XTK28jzYkvqYBtPuv0H5AaGlfig4JIiPWGLE9EgA+wZxeEB6ZTRA==" + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@fal-ai/serverless-client/-/serverless-client-0.8.2.tgz", + "integrity": "sha512-oGT7Vi6S+CviOTsIEAf6isnLEp6g4gQEPBE6SqJUlbVIGFDzQQdXZr9bdToGXIomYXassD9waFfPBedxUVCKtA==", + "dependencies": { + "@msgpack/msgpack": "^3.0.0-beta2", + "robot3": "^0.4.1", + "uuid-random": "^1.3.2" + }, + "engines": { + "node": ">=18.0.0" + } }, "node_modules/@fal-ai/serverless-proxy": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@fal-ai/serverless-proxy/-/serverless-proxy-0.6.0.tgz", - "integrity": "sha512-fu1IOKTvwa1x5oJh/BffKav60lz89Uw8M/6SP6imkrphnDRjsc53unr0qCs3UN3NSuHs8syG/xZ9KntORvbq5g==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@fal-ai/serverless-proxy/-/serverless-proxy-0.7.3.tgz", + "integrity": "sha512-iTr+1UJFUpEKR4gKBGFcsw3X4fl7mcYFbN4VXtrTGppkqZYd9oa4bND7Q8ANKCKqOn6hYzuwY+qGbGjzV6tnWw==", "peerDependencies": { "express": "^4.0.0", "next": "13.4 - 14", @@ -204,6 +212,14 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@msgpack/msgpack": { + "version": "3.0.0-beta2", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.0.0-beta2.tgz", + "integrity": "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==", + "engines": { + "node": ">= 14" + } + }, "node_modules/@next/env": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz", @@ -4574,6 +4590,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robot3": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz", + "integrity": "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5159,6 +5180,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuid-random": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", + "integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==" + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 6123362..a8669d9 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "format": "prettier --write ." }, "dependencies": { - "@fal-ai/serverless-client": "^0.6.0", - "@fal-ai/serverless-proxy": "^0.6.0", + "@fal-ai/serverless-client": "^0.8.2", + "@fal-ai/serverless-proxy": "^0.7.3", "@tldraw/tldraw": "canary", "next": "14.0.3", "react": "^18", @@ -32,4 +32,4 @@ "prettier-plugin-organize-imports": "^3.2.4", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c5371a8..f414657 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,8 +4,8 @@ import './globals.css' const inter = Inter({ subsets: ['latin'] }) -const TITLE = 'draw fast • tldraw' -const DESCRIPTION = 'Draw a picture (fast) with tldraw' +const TITLE = 'draw faster • tldraw' +const DESCRIPTION = 'Draw a picture (faster) with tldraw' const TWITTER_HANDLE = '@tldraw' const TWITTER_CARD = 'social-twitter.png' const FACEBOOK_CARD = 'social-og.png' diff --git a/src/app/page.tsx b/src/app/page.tsx index c3c12ba..0a07bf3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,13 +1,13 @@ /* eslint-disable @next/next/no-img-element */ 'use client' +import { CameraFeedShapeUtil } from '@/components/CameraFeedShapeUtil' import { LiveImageShape, LiveImageShapeUtil } from '@/components/LiveImageShapeUtil' -import { LiveImageTool,MakeLiveButton } from '@/components/LiveImageTool' +import { LiveImageTool, MakeLiveButton } from '@/components/LiveImageTool' import { LockupLink } from '@/components/LockupLink' import { LiveImageProvider } from '@/hooks/useLiveImage' import * as fal from '@fal-ai/serverless-client' import { - AssetRecordType, DefaultSizeStyle, Editor, TLUiOverrides, @@ -20,9 +20,7 @@ import { useEffect, useMemo, useState } from 'react' import { createPortal } from 'react-dom' fal.config({ - requestMiddleware: fal.withProxy({ - targetUrl: '/api/fal/proxy', - }), + proxyUrl: '/api/fal/proxy', }) const overrides: TLUiOverrides = { @@ -53,7 +51,7 @@ const overrides: TLUiOverrides = { }, } -const shapeUtils = [LiveImageShapeUtil] +const shapeUtils = [LiveImageShapeUtil, CameraFeedShapeUtil] const tools = [LiveImageTool] export default function Home() { @@ -86,11 +84,11 @@ export default function Home() { } return ( - +
+ +export class CameraFeedShapeUtil extends ShapeUtil { + static type = 'camera-feed' as any + + override canBind = () => false + override canUnmount = () => false + override isAspectRatioLocked = () => false + + getDefaultProps() { + return { + // 16 by 9 + w: 512, + h: 360, + name: '', + } + } + + override getGeometry(shape: CameraFeedShape): Geometry2d { + return new Rectangle2d({ + width: shape.props.w, + height: shape.props.h, + isFilled: true, + }) + } + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info) + } + + indicator(shape: CameraFeedShape) { + const bounds = this.editor.getShapeGeometry(shape).bounds + + return + } + + ref: HTMLVideoElement | null = null + + override component(shape: CameraFeedShape) { + const videoRef = useRef(null) + + this.ref = videoRef.current + + // Get the user's camera + useEffect(() => { + const constraints = { + video: { + width: shape.props.w, + height: shape.props.h, + }, + } + navigator.mediaDevices + .getUserMedia({ video: constraints as any }) + .then((stream) => { + if (videoRef.current) { + videoRef.current.srcObject = stream + } + }) + .catch((error) => { + console.error('Error accessing media devices.', error) + }) + }, [shape.props.w, shape.props.h]) + + return ( + + + ) + } + + override toSvg(shape: CameraFeedShape) { + // get an image of the video stream + + const video = this.ref + if (!video) throw new Error('Video ref not found') + if (canvas.width !== shape.props.w || canvas.height !== shape.props.h) { + canvas.width = shape.props.w * 2 + canvas.height = shape.props.h * 2 + } + // flip horizontally + ctx?.scale(-1, 1) + ctx?.translate(-shape.props.w, 0) + ctx?.drawImage(video, 0, 0, shape.props.w, shape.props.h) + const dataUrl = canvas.toDataURL('image/png') + const svgImageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image') + svgImageElement.setAttribute('href', dataUrl) + // svgImageElement.setAttribute('width', shape.props.w.toString()) + // svgImageElement.setAttribute('height', shape.props.h.toString()) + // svgImageElement.setAttribute('x', '-100') + // svgImageElement.setAttribute('y', '0') + return svgImageElement + } +} + +let canvas = null +let ctx = null +if (globalThis.window) { + canvas = document.createElement('canvas') + ctx = canvas.getContext('2d') + canvas.width = 512 + canvas.height = 512 +} diff --git a/src/components/LiveImageShapeUtil.tsx b/src/components/LiveImageShapeUtil.tsx index ea0d5e1..f814624 100644 --- a/src/components/LiveImageShapeUtil.tsx +++ b/src/components/LiveImageShapeUtil.tsx @@ -53,6 +53,7 @@ export type LiveImageShape = TLBaseShape< h: number name: string overlayResult?: boolean + src?: string } > @@ -178,20 +179,21 @@ export class LiveImageShapeUtil extends ShapeUtil { width={bounds.width} height={bounds.height} /> - {!shape.props.overlayResult && asset && asset.props.src && ( - {shape.props.name} - )} + + {shape.props.name} + - ); + return ( + + ) } diff --git a/src/hooks/useLiveImage.tsx b/src/hooks/useLiveImage.tsx index 0f1470c..4079035 100644 --- a/src/hooks/useLiveImage.tsx +++ b/src/hooks/useLiveImage.tsx @@ -1,145 +1,151 @@ import { LiveImageShape } from '@/components/LiveImageShapeUtil' -import { blobToDataUri } from '@/utils/blob' import * as fal from '@fal-ai/serverless-client' -import { - AssetRecordType, - Editor, - TLShape, - TLShapeId, - getHashForObject, - getSvgAsImage, - rng, - useEditor, -} from '@tldraw/tldraw' -import { createContext, useContext, useEffect, useState } from 'react' -import { v4 as uuid } from 'uuid' - -type LiveImageResult = { url: string } -type LiveImageRequest = { +import { RealtimeConnection } from '@fal-ai/serverless-client/src/realtime' +import { Editor, TLShape, TLShapeId, getHashForObject, useEditor } from '@tldraw/tldraw' +import { createContext, useContext, useEffect, useRef, useState } from 'react' + +type LCMInput = { prompt: string - image_url: string - sync_mode: boolean - strength: number + image: Uint8Array + strength?: number + negative_prompt?: string + seed?: number | null + guidance_scale?: number + num_inference_steps?: number + enable_safety_checks?: boolean + request_id?: string + height?: number + width?: number +} + +type LCMOutput = { + image: Uint8Array + timings: Record seed: number - enable_safety_checks: boolean + num_inference_steps: number + request_id: string + nsfw_content_detected: boolean[] } -type LiveImageContextType = null | ((req: LiveImageRequest) => Promise) + +type Send = (req: LCMInput) => void + +type LiveImageContextType = RealtimeConnection | null const LiveImageContext = createContext(null) -export function LiveImageProvider({ - children, - appId, - throttleTime = 0, - timeoutTime = 5000, -}: { - children: React.ReactNode - appId: string - throttleTime?: number - timeoutTime?: number -}) { - const [count, setCount] = useState(0) - const [fetchImage, setFetchImage] = useState<{ current: LiveImageContextType }>({ current: null }) +export function LiveImageProvider({ children }: { children: React.ReactNode }) { + // const [count, setCount] = useState(0) + // const [fetchImage, setFetchImage] = useState<{ current: LiveImageContextType }>({ current: null }) + + const [connection, setConnection] = useState | null>(null) useEffect(() => { - const requestsById = new Map< - string, + // const requestsById = new Map< + // string, + // { + // resolve: (result: LiveImageResult) => void + // reject: (err: unknown) => void + // timer: ReturnType + // } + // >() + + const _connection = fal.realtime.connect( + 'fal-ai/sd-turbo-real-time-high-fps-msgpack-a10g', + // '110602490-lcm-sd15-i2i', + // 'fal-ai/lcm-sd15-i2i', { - resolve: (result: LiveImageResult) => void - reject: (err: unknown) => void - timer: ReturnType - } - >() - - const { send, close } = fal.realtime.connect(appId, { - connectionKey: 'fal-realtime-example', - clientOnly: false, - throttleInterval: throttleTime, - onError: (error) => { - console.error(error) - // force re-connect - setCount((count) => count + 1) - }, - onResult: (result) => { - if (result.images && result.images[0]) { - const id = result.request_id - const request = requestsById.get(id) - if (request) { - request.resolve(result.images[0]) + connectionKey: 'draw-faster', + throttleInterval: 0, + onError: (error) => { + console.error(error) + }, + onResult: (result) => { + if (result.image) { + const blob = new Blob([result.image], { type: 'image/jpeg' }) + const url = URL.createObjectURL(blob) + // @ts-expect-error: yolo + updateGeneratedImage(window.editor, result.request_id as TLShapeId, url) } - } - }, - }) - - setFetchImage({ - current: (req) => { - return new Promise((resolve, reject) => { - const id = uuid() - const timer = setTimeout(() => { - requestsById.delete(id) - reject(new Error('Timeout')) - }, timeoutTime) - requestsById.set(id, { - resolve: (res) => { - resolve(res) - clearTimeout(timer) - }, - reject: (err) => { - reject(err) - clearTimeout(timer) - }, - timer, - }) - send({ ...req, request_id: id }) - }) - }, - }) - return () => { - for (const request of requestsById.values()) { - request.reject(new Error('Connection closed')) - } - try { - close() - } catch (e) { - // noop + // console.log(result) + // if (result.images && result.images[0]) { + // const id = result.request_id + // const request = requestsById.get(id) + // if (request) { + // request.resolve(result.images[0]) + // } + // } + }, } + ) + + setConnection(_connection) + + // setSend(connection.send) + + // setFetchImage({ + // current: (req) => { + // return new Promise((resolve, reject) => { + // const id = uuid() + // const timer = setTimeout(() => { + // requestsById.delete(id) + // reject(new Error('Timeout')) + // }, timeoutTime) + // requestsById.set(id, { + // resolve: (res) => { + // resolve(res) + // clearTimeout(timer) + // }, + // reject: (err) => { + // reject(err) + // clearTimeout(timer) + // }, + // timer, + // }) + // send({ ...req, request_id: id }) + // }) + // }, + // }) + + return () => { + _connection.close() + setConnection(null) } - }, [appId, count, throttleTime, timeoutTime]) + }, []) - return ( - {children} - ) + return {children} } -export function useLiveImage( - shapeId: TLShapeId, - { throttleTime = 64 }: { throttleTime?: number } = {} -) { +export function useLiveImage(shapeId: TLShapeId) { const editor = useEditor() - const fetchImage = useContext(LiveImageContext) - if (!fetchImage) throw new Error('Missing LiveImageProvider') - + const connection = useContext(LiveImageContext) + const intervalRef = useRef(null) + if (!connection) throw new Error('Missing LiveImageProvider') + const send = connection.send + if (!send) throw new Error('Missing LiveImageProvider') useEffect(() => { + // if (!document) return + const _canvas = document.createElement('canvas') + const _ctx = _canvas.getContext('2d')! + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + let prevHash = '' let prevPrompt = '' - let startedIteration = 0 let finishedIteration = 0 - async function updateDrawing() { + if (!send) throw new Error('Missing LiveImageProvider') const shapes = getShapesTouching(shapeId, editor) const frame = editor.getShape(shapeId)! - const hash = getHashForObject([...shapes]) const frameName = frame.props.name - if (hash === prevHash && frameName === prevPrompt) return + // if (hash === prevHash && frameName === prevPrompt) return startedIteration += 1 const iteration = startedIteration - prevHash = hash prevPrompt = frame.props.name - try { const svg = await editor.getSvg([...shapes], { background: true, @@ -149,117 +155,252 @@ export function useLiveImage( }) // cancel if stale: if (iteration <= finishedIteration) return - if (!svg) { console.error('No SVG') - updateImage(editor, frame.id, '') + updateGeneratedImage(editor, frame.id, '') return } - - const image = await getSvgAsImage(svg, editor.environment.isSafari, { - type: 'png', - quality: 1, + const blobPromise = _getSvgAsImage(svg, editor.environment.isSafari, _canvas, _ctx, { + type: 'jpeg', + quality: 0.5, scale: 512 / frame.props.w, + // scale: 256 / frame.props.w, }) - // cancel if stale: - if (iteration <= finishedIteration) return - if (!image) { - console.error('No image') - updateImage(editor, frame.id, '') - return - } + blobPromise.then(async (blob) => { + // cancel if stale: + if (iteration <= finishedIteration) return - const prompt = frameName - ? frameName + ' hd award-winning impressive' - : 'A random image that is safe for work and not surprising—something boring like a city or shoe watercolor' + if (!blob) { + console.error('No image') + updateGeneratedImage(editor, frame.id, '') + return + } - const imageDataUri = await blobToDataUri(image) + // cancel if stale: + if (iteration <= finishedIteration) return - // cancel if stale: - if (iteration <= finishedIteration) return + const buffer = await blob.arrayBuffer() - // downloadDataURLAsFile(imageDataUri, 'image.png') + // cancel if stale: + if (iteration <= finishedIteration) return - const random = rng(shapeId) + const data = new Uint8Array(buffer) + // const prompt = frameName ? frameName : 'A person' - const result = await fetchImage!({ - prompt, - image_url: imageDataUri, - sync_mode: true, - strength: 0.65, - seed: Math.abs(random() * 10000), // TODO make this configurable in the UI - enable_safety_checks: false, - }) - // cancel if stale: - if (iteration <= finishedIteration) return + const prompt = frameName + ? frameName + '' + : 'A random image that is safe for work and not surprising—something boring like a city or shoe watercolor' + + // downloadDataURLAsFile(imageDataUri, 'image.png') + send({ + prompt, + image: data, + seed: 42, + enable_safety_checks: false, + guidance_scale: 1, + request_id: shapeId, + negative_prompt: 'woman child human girl man person face', + // temperature: 0, + + // strength: 0.666666, + // strength: 0.6666666666666666, + // num_inference_steps: 2, + num_inference_steps: 3, - finishedIteration = iteration - updateImage(editor, frame.id, result.url) + // strength: 0.6677, + strength: 0.6666666677, + // strength: 0.999999999, + + // strength: 0.7, + // num_inference_steps: 3, + }) + + // cancel if stale: + if (iteration <= finishedIteration) return + finishedIteration = iteration + }) } catch (e) { - const isTimeout = e instanceof Error && e.message === 'Timeout' - if (!isTimeout) { - console.error(e) - } + throw e + } + } - // retry if this was the most recent request: - if (iteration === startedIteration) { - requestUpdate() - } + // let timer: ReturnType | null = null + // function requestUpdate() { + // // updateDrawing() + // console.log('send' + Math.random()) + // if (timer !== null) return + // timer = setTimeout(() => { + // timer = null + // updateDrawing() + // }, 16) + // } + + intervalRef.current = setInterval(() => { + updateDrawing() + }, 128) + // editor.on('update-drawings' as any, requestUpdate) + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) } + // editor.off('update-drawings' as any, requestUpdate) } + }, [editor, shapeId, send]) +} - let timer: ReturnType | null = null - function requestUpdate() { - if (timer !== null) return - timer = setTimeout(() => { - timer = null - updateDrawing() - }, throttleTime) +export async function _getSvgAsImage( + svg: SVGElement, + isSafari: boolean, + _canvas: HTMLCanvasElement, + _ctx: CanvasRenderingContext2D, + options: { + type: 'png' | 'jpeg' | 'webp' + quality: number + scale: number + } +) { + const { type, quality, scale } = options + + const width = +svg.getAttribute('width')! + const height = +svg.getAttribute('height')! + let [clampedWidth, clampedHeight] = [width * scale, height * scale] + clampedWidth = Math.floor(clampedWidth) + clampedHeight = Math.floor(clampedHeight) + + const svgString = await _getSvgAsString(svg) + const svgUrl = URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml' })) + + const canvas = await new Promise((resolve) => { + const image = new Image() + image.crossOrigin = 'anonymous' + + image.onload = async () => { + const canvas = _canvas + const ctx = _ctx + + canvas.width = clampedWidth + canvas.height = clampedHeight + + ctx.imageSmoothingEnabled = true + ctx.imageSmoothingQuality = 'high' + ctx.drawImage(image, 0, 0, clampedWidth, clampedHeight) + + URL.revokeObjectURL(svgUrl) + + resolve(canvas) } - editor.on('update-drawings' as any, requestUpdate) - return () => { - editor.off('update-drawings' as any, requestUpdate) + image.onerror = () => { + resolve(null) } - }, [editor, fetchImage, shapeId, throttleTime]) + + image.src = svgUrl + }) + + if (!canvas) return null + + const blobPromise = new Promise((resolve) => + canvas.toBlob( + (blob) => { + if (!blob) { + resolve(null) + } + resolve(blob) + }, + 'image/' + type, + quality + ) + ) + + return blobPromise + // const view = new DataView(await blob.arrayBuffer()) + // return PngHelpers.setPhysChunk(view, effectiveScale, { + // type: 'image/' + type, + // }) +} + +async function _getSvgAsString(svg: SVGElement) { + const clone = svg.cloneNode(true) as SVGGraphicsElement + + svg.setAttribute('width', +svg.getAttribute('width')! + '') + svg.setAttribute('height', +svg.getAttribute('height')! + '') + + const fileReader = new FileReader() + const imgs = Array.from(clone.querySelectorAll('image')) as SVGImageElement[] + + for (const img of imgs) { + const src = img.getAttribute('xlink:href') + if (src) { + if (!src.startsWith('data:')) { + const blob = await (await fetch(src)).blob() + const base64 = await new Promise((resolve, reject) => { + fileReader.onload = () => resolve(fileReader.result as string) + fileReader.onerror = () => reject(fileReader.error) + fileReader.readAsDataURL(blob) + }) + img.setAttribute('xlink:href', base64) + } + } + } + + const out = new XMLSerializer() + .serializeToString(clone) + .replaceAll(' ', '') + .replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1') + + return out } -function updateImage(editor: Editor, shapeId: TLShapeId, url: string | null) { +function updateGeneratedImage(editor: Editor, shapeId: TLShapeId, url: string | null) { const shape = editor.getShape(shapeId)! - const id = AssetRecordType.createId(shape.id.split(':')[1]) - - const asset = editor.getAsset(id) - - if (!asset) { - editor.createAssets([ - AssetRecordType.create({ - id, - type: 'image', - props: { - name: shape.props.name, - w: shape.props.w, - h: shape.props.h, - src: url, - isAnimated: false, - mimeType: 'image/jpeg', - }, - }), - ]) - } else { - editor.updateAssets([ - { - ...asset, - type: 'image', - props: { - ...asset.props, - w: shape.props.w, - h: shape.props.h, - src: url, - }, - }, - ]) + + if (!url) { + url = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' } + + editor.updateShape({ + id: shape.id, + type: shape.type, + props: { + ...shape.props, + src: url, + }, + }) + + // const id = AssetRecordType.createId(shape.id.split(':')[1]) + + // const asset = editor.getAsset(id) + + // if (!asset) { + // editor.createAssets([ + // AssetRecordType.create({ + // id, + // type: 'image', + // props: { + // name: shape.props.name, + // w: shape.props.w, + // h: shape.props.h, + // src: url, + // isAnimated: false, + // mimeType: 'image/jpeg', + // }, + // }), + // ]) + // } else { + // editor.updateAssets([ + // { + // ...asset, + // type: 'image', + // props: { + // ...asset.props, + // w: shape.props.w, + // h: shape.props.h, + // src: url, + // }, + // }, + // ]) + // } } function getShapesTouching(shapeId: TLShapeId, editor: Editor) {