This commit is contained in:
Steve Ruiz 2023-11-25 19:49:34 +00:00
parent 63ccc75ac6
commit d1565789fd
7 changed files with 92 additions and 141 deletions

11
.prettierrc Normal file
View File

@ -0,0 +1,11 @@
{
"trailingComma": "es5",
"singleQuote": true,
"semi": false,
"printWidth": 100,
"tabWidth": 2,
"useTabs": true,
"plugins": [
"prettier-plugin-organize-imports"
]
}

30
package-lock.json generated
View File

@ -7,8 +7,9 @@
"": { "": {
"name": "tldraw-fal", "name": "tldraw-fal",
"version": "0.1.0", "version": "0.1.0",
"license": "MIT",
"dependencies": { "dependencies": {
"@fal-ai/serverless-client": "^0.5.4", "@fal-ai/serverless-client": "^0.6.0-alpha.4",
"@fal-ai/serverless-proxy": "^0.5.0", "@fal-ai/serverless-proxy": "^0.5.0",
"@tldraw/tldraw": "^2.0.0-canary.ba4091c59418", "@tldraw/tldraw": "^2.0.0-canary.ba4091c59418",
"next": "14.0.3", "next": "14.0.3",
@ -24,6 +25,7 @@
"eslint-config-next": "14.0.3", "eslint-config-next": "14.0.3",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"prettier-plugin-organize-imports": "^3.2.4",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "^5" "typescript": "^5"
} }
@ -117,9 +119,9 @@
} }
}, },
"node_modules/@fal-ai/serverless-client": { "node_modules/@fal-ai/serverless-client": {
"version": "0.5.4", "version": "0.6.0-alpha.4",
"resolved": "https://registry.npmjs.org/@fal-ai/serverless-client/-/serverless-client-0.5.4.tgz", "resolved": "https://registry.npmjs.org/@fal-ai/serverless-client/-/serverless-client-0.6.0-alpha.4.tgz",
"integrity": "sha512-8eTA+lBXtGzYqDTjIHm7vnViu5AlPZYR1D9BQbsDxro/K53W9sMhZtc7QSboE1MMcbs4hW2B+f+Jkkx39hJCPw==" "integrity": "sha512-T9fqiMU1LohzzqsNZjY4zq8ZMxJcZo8eYvSXaz2i6nmb9sQxuQ4hCB/YTeZJ2Ong4P+ZOMkHPMD2kW4pNgf5gw=="
}, },
"node_modules/@fal-ai/serverless-proxy": { "node_modules/@fal-ai/serverless-proxy": {
"version": "0.5.0", "version": "0.5.0",
@ -4642,6 +4644,26 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prettier-plugin-organize-imports": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
"dev": true,
"peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4",
"@volar/vue-typescript": "^1.0.4",
"prettier": ">=2.0",
"typescript": ">=2.9"
},
"peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": {
"optional": true
},
"@volar/vue-typescript": {
"optional": true
}
}
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",

View File

@ -2,6 +2,7 @@
"name": "tldraw-fal", "name": "tldraw-fal",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"license": "MIT",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
@ -10,7 +11,7 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@fal-ai/serverless-client": "^0.5.4", "@fal-ai/serverless-client": "^0.6.0-alpha.4",
"@fal-ai/serverless-proxy": "^0.5.0", "@fal-ai/serverless-proxy": "^0.5.0",
"@tldraw/tldraw": "^2.0.0-canary.ba4091c59418", "@tldraw/tldraw": "^2.0.0-canary.ba4091c59418",
"next": "14.0.3", "next": "14.0.3",
@ -26,7 +27,8 @@
"eslint-config-next": "14.0.3", "eslint-config-next": "14.0.3",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.1.0", "prettier": "^3.1.0",
"prettier-plugin-organize-imports": "^3.2.4",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "^5" "typescript": "^5"
} }
} }

View File

@ -1,18 +1,9 @@
'use client' 'use client'
import { import { LiveImageShape, LiveImageShapeUtil } from '@/components/LiveImageShapeUtil'
LiveImageShape,
LiveImageShapeUtil,
} from '@/components/LiveImageShapeUtil'
import * as fal from '@fal-ai/serverless-client' import * as fal from '@fal-ai/serverless-client'
import { import { Editor, Tldraw, useEditor } from '@tldraw/tldraw'
AssetRecordType, import { useEffect } from 'react'
Editor,
FrameShapeTool,
Tldraw,
useEditor,
} from '@tldraw/tldraw'
import { useCallback, useEffect } from 'react'
import { LiveImageTool, MakeLiveButton } from '../components/LiveImageTool' import { LiveImageTool, MakeLiveButton } from '../components/LiveImageTool'
fal.config({ fal.config({

View File

@ -8,9 +8,9 @@ import {
useIsEditing, useIsEditing,
useValue, useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import { preventDefault, stopEventPropagation } from '@tldraw/tldraw'
import { useCallback, useEffect, useRef } from 'react' import { useCallback, useEffect, useRef } from 'react'
import { FrameLabelInput } from './FrameLabelInput' import { FrameLabelInput } from './FrameLabelInput'
import { preventDefault, stopEventPropagation } from '@tldraw/tldraw'
export function FrameHeading({ export function FrameHeading({
id, id,
@ -41,8 +41,6 @@ export function FrameHeading({
const event = getPointerInfo(e) const event = getPointerInfo(e)
console.log('hello')
// If we're editing the frame label, we shouldn't hijack the pointer event // If we're editing the frame label, we shouldn't hijack the pointer event
if (editor.getEditingShapeId() === id) return if (editor.getEditingShapeId() === id) return
@ -77,9 +75,9 @@ export function FrameHeading({
// rotate right 45 deg // rotate right 45 deg
const offsetRotation = pageRotation + Math.PI / 4 const offsetRotation = pageRotation + Math.PI / 4
const scaledRotation = (offsetRotation * (2 / Math.PI) + 4) % 4 const scaledRotation = (offsetRotation * (2 / Math.PI) + 4) % 4
const labelSide: SelectionEdge = ( const labelSide: SelectionEdge = (['top', 'left', 'bottom', 'right'] as const)[
['top', 'left', 'bottom', 'right'] as const Math.floor(scaledRotation)
)[Math.floor(scaledRotation)] ]
let labelTranslate: string let labelTranslate: string
switch (labelSide) { switch (labelSide) {
@ -87,9 +85,7 @@ export function FrameHeading({
labelTranslate = `` labelTranslate = ``
break break
case 'right': case 'right':
labelTranslate = `translate(${toDomPrecision( labelTranslate = `translate(${toDomPrecision(width)}px, 0px) rotate(90deg)`
width
)}px, 0px) rotate(90deg)`
break break
case 'bottom': case 'bottom':
labelTranslate = `translate(${toDomPrecision(width)}px, ${toDomPrecision( labelTranslate = `translate(${toDomPrecision(width)}px, ${toDomPrecision(
@ -97,9 +93,7 @@ export function FrameHeading({
)}px) rotate(180deg)` )}px) rotate(180deg)`
break break
case 'left': case 'left':
labelTranslate = `translate(0px, ${toDomPrecision( labelTranslate = `translate(0px, ${toDomPrecision(height)}px) rotate(270deg)`
height
)}px) rotate(270deg)`
break break
} }
@ -109,9 +103,7 @@ export function FrameHeading({
style={{ style={{
overflow: isEditing ? 'visible' : 'hidden', overflow: isEditing ? 'visible' : 'hidden',
maxWidth: `calc(var(--tl-zoom) * ${ maxWidth: `calc(var(--tl-zoom) * ${
labelSide === 'top' || labelSide === 'bottom' labelSide === 'top' || labelSide === 'bottom' ? Math.ceil(width) : Math.ceil(height)
? Math.ceil(width)
: Math.ceil(height)
}px + var(--space-5))`, }px + var(--space-5))`,
bottom: '100%', bottom: '100%',
transform: `${labelTranslate} scale(var(--tl-scale)) translateX(calc(-1 * var(--space-3))`, transform: `${labelTranslate} scale(var(--tl-scale)) translateX(calc(-1 * var(--space-3))`,
@ -119,12 +111,7 @@ export function FrameHeading({
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
> >
<div className="tl-frame-heading-hit-area"> <div className="tl-frame-heading-hit-area">
<FrameLabelInput <FrameLabelInput ref={rInput} id={id} name={name} isEditing={isEditing} />
ref={rInput}
id={id}
name={name}
isEditing={isEditing}
/>
</div> </div>
</div> </div>
) )

View File

@ -2,20 +2,12 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { import {
AssetRecordType, AssetRecordType,
canonicalizeRotation,
FrameShapeUtil,
Geometry2d, Geometry2d,
getDefaultColorTheme, getDefaultColorTheme,
getHashForObject,
getSvgAsImage,
HTMLContainer,
IdOf,
Rectangle2d, Rectangle2d,
resizeBox, resizeBox,
SelectionEdge,
ShapeUtil, ShapeUtil,
SVGContainer, SVGContainer,
TLAsset,
TLBaseShape, TLBaseShape,
TLGroupShape, TLGroupShape,
TLOnResizeEndHandler, TLOnResizeEndHandler,
@ -27,15 +19,8 @@ import {
useIsDarkMode, useIsDarkMode,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { blobToDataUri } from '@/utils/blob'
import { debounce } from '@/utils/debounce'
import * as fal from '@fal-ai/serverless-client'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import result from 'postcss/lib/result'
import { FrameHeading } from './FrameHeading'
import image from 'next/image'
import { connect } from 'http2'
import { useFal } from '@/hooks/useFal' import { useFal } from '@/hooks/useFal'
import { FrameHeading } from './FrameHeading'
// See https://www.fal.ai/models/latent-consistency-sd // See https://www.fal.ai/models/latent-consistency-sd
@ -94,10 +79,7 @@ export class LiveImageShapeUtil extends ShapeUtil<LiveImageShape> {
canUnmount = () => false canUnmount = () => false
override canReceiveNewChildrenOfType = ( override canReceiveNewChildrenOfType = (shape: TLShape, _type: TLShape['type']) => {
shape: TLShape,
_type: TLShape['type']
) => {
return !shape.isLocked return !shape.isLocked
} }
@ -105,10 +87,7 @@ export class LiveImageShapeUtil extends ShapeUtil<LiveImageShape> {
return true return true
} }
override canDropShapes = ( override canDropShapes = (shape: LiveImageShape, _shapes: TLShape[]): boolean => {
shape: LiveImageShape,
_shapes: TLShape[]
): boolean => {
return !shape.isLocked return !shape.isLocked
} }
@ -126,13 +105,9 @@ export class LiveImageShapeUtil extends ShapeUtil<LiveImageShape> {
return { shouldHint: false } return { shouldHint: false }
} }
override onDragShapesOut = ( override onDragShapesOut = (_shape: LiveImageShape, shapes: TLShape[]): void => {
_shape: LiveImageShape,
shapes: TLShape[]
): void => {
const parent = this.editor.getShape(_shape.parentId) const parent = this.editor.getShape(_shape.parentId)
const isInGroup = const isInGroup = parent && this.editor.isShapeOfType<TLGroupShape>(parent, 'group')
parent && this.editor.isShapeOfType<TLGroupShape>(parent, 'group')
// If frame is in a group, keep the shape // If frame is in a group, keep the shape
// moved out in that group // moved out in that group
@ -158,10 +133,7 @@ export class LiveImageShapeUtil extends ShapeUtil<LiveImageShape> {
} }
if (shapesToReparent.length > 0) { if (shapesToReparent.length > 0) {
this.editor.reparentShapes( this.editor.reparentShapes(shapesToReparent, this.editor.getCurrentPageId())
shapesToReparent,
this.editor.getCurrentPageId()
)
} }
} }
@ -186,7 +158,7 @@ export class LiveImageShapeUtil extends ShapeUtil<LiveImageShape> {
useFal(shape.id, { useFal(shape.id, {
debounceTime: 0, debounceTime: 0,
url: 'wss://110602490-lcm-sd15-i2i.gateway.alpha.fal.ai/ws', appId: '110602490-lcm-sd15-i2i',
}) })
const bounds = this.editor.getShapeGeometry(shape).bounds const bounds = this.editor.getShapeGeometry(shape).bounds

View File

@ -1,5 +1,6 @@
import { LiveImageShape } from '@/components/LiveImageShapeUtil' import { LiveImageShape } from '@/components/LiveImageShapeUtil'
import { blobToDataUri } from '@/utils/blob' import { blobToDataUri } from '@/utils/blob'
import * as fal from '@fal-ai/serverless-client'
import { import {
AssetRecordType, AssetRecordType,
TLShape, TLShape,
@ -10,17 +11,17 @@ import {
throttle, throttle,
useEditor, useEditor,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useRef, useEffect } from 'react' import { useEffect, useRef } from 'react'
export function useFal( export function useFal(
shapeId: TLShapeId, shapeId: TLShapeId,
opts: { opts: {
debounceTime?: number debounceTime?: number
throttleTime?: number throttleTime?: number
url: string appId: string
} }
) { ) {
const { url, throttleTime = 500, debounceTime = 0 } = opts const { appId, throttleTime = 500, debounceTime = 0 } = opts
const editor = useEditor() const editor = useEditor()
const startedIteration = useRef<number>(0) const startedIteration = useRef<number>(0)
const finishedIteration = useRef<number>(0) const finishedIteration = useRef<number>(0)
@ -28,10 +29,6 @@ export function useFal(
const prevHash = useRef<string | null>(null) const prevHash = useRef<string | null>(null)
useEffect(() => { useEffect(() => {
let socket: WebSocket | null = null
let isReconnecting = false
function updateImage(url: string | null) { function updateImage(url: string | null) {
const shape = editor.getShape<LiveImageShape>(shapeId)! const shape = editor.getShape<LiveImageShape>(shapeId)!
const id = AssetRecordType.createId(shape.id.split(':')[1]) const id = AssetRecordType.createId(shape.id.split(':')[1])
@ -69,60 +66,26 @@ export function useFal(
} }
} }
async function connect() { const { send: sendCurrentData, close } = fal.realtime.connect(appId, {
{ connectionKey: 'fal-realtime-example',
socket = new WebSocket(url) clientOnly: false,
socket.onopen = () => { throttleInterval: 1000,
// console.log("WebSocket Open"); onError: (error) => {
console.error(error)
},
onResult: (result) => {
console.log(result)
if (result.images && result.images[0]) {
updateImage(result.images[0].url)
} }
},
socket.onclose = () => { })
// console.log("WebSocket Close");
}
socket.onerror = (error) => {
// console.error("WebSocket Error:", error);
}
socket.onmessage = (message) => {
try {
const data = JSON.parse(message.data)
// console.log("WebSocket Message:", data);
if (data.images && data.images.length > 0) {
updateImage(data.images[0].url ?? '')
}
} catch (e) {
console.error('Error parsing the WebSocket response:', e)
}
}
}
}
async function sendCurrentData(message: string) {
if (!isReconnecting && socket?.readyState !== WebSocket.OPEN) {
isReconnecting = true
connect()
}
if (isReconnecting && socket?.readyState !== WebSocket.OPEN) {
await new Promise<void>((resolve) => {
const checkConnection = setInterval(() => {
if (socket?.readyState === WebSocket.OPEN) {
clearInterval(checkConnection)
resolve()
}
}, 100)
})
isReconnecting = false
}
socket?.send(message)
}
async function updateDrawing() { async function updateDrawing() {
const iteration = startedIteration.current++ const iteration = startedIteration.current++
const shapes = Array.from(editor.getShapeAndDescendantIds([shapeId])).map( const shapes = Array.from(editor.getShapeAndDescendantIds([shapeId])).map((id) =>
(id) => editor.getShape(id) editor.getShape(id)
) as TLShape[] ) as TLShape[]
const hash = getHashForObject(shapes) const hash = getHashForObject(shapes)
@ -160,18 +123,20 @@ export function useFal(
? shape.props.name + ' hd award-winning impressive' ? shape.props.name + ' hd award-winning impressive'
: 'A random image that is safe for work and not surprising—something boring like a city or shoe watercolor' : 'A random image that is safe for work and not surprising—something boring like a city or shoe watercolor'
const imageDataUri = await blobToDataUri(image) const imageDataUri = await blobToDataUri(image)
const request = {
image_url: imageDataUri,
prompt,
sync_mode: true,
strength: 0.7,
seed: 42, // TODO make this configurable in the UI
enable_safety_checks: false,
}
// We might be stale // We might be stale
if (iteration <= finishedIteration.current) return if (iteration <= finishedIteration.current) return
sendCurrentData(JSON.stringify(request))
console.log('ok here we go')
sendCurrentData(
JSON.stringify({
prompt,
image_url: imageDataUri,
sync_mode: true,
strength: 0.7,
seed: 11252023, // TODO make this configurable in the UI
enable_safety_checks: false,
})
)
finishedIteration.current = iteration finishedIteration.current = iteration
} }
@ -184,7 +149,8 @@ export function useFal(
editor.on('update-drawings' as any, onDrawingChange) editor.on('update-drawings' as any, onDrawingChange)
return () => { return () => {
close()
editor.off('update-drawings' as any, onDrawingChange) editor.off('update-drawings' as any, onDrawingChange)
} }
}, [editor, shapeId, throttleTime, debounceTime, url]) }, [editor, shapeId, throttleTime, debounceTime, appId])
} }