commit
4668ba8bf7
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": true,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-organize-imports"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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({
|
||||||
|
|
@ -26,6 +17,7 @@ const tools = [LiveImageTool]
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const onEditorMount = (editor: Editor) => {
|
const onEditorMount = (editor: Editor) => {
|
||||||
|
// We need the editor to think that the live image shape is a frame
|
||||||
// @ts-expect-error: patch
|
// @ts-expect-error: patch
|
||||||
editor.isShapeOfType = function (arg, type) {
|
editor.isShapeOfType = function (arg, type) {
|
||||||
const shape = typeof arg === 'string' ? this.getShape(arg)! : arg
|
const shape = typeof arg === 'string' ? this.getShape(arg)! : arg
|
||||||
|
|
@ -36,24 +28,18 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there isn't a live image shape, create one
|
// If there isn't a live image shape, create one
|
||||||
const liveImage = editor.getCurrentPageShapes().find((shape) => {
|
if (!editor.getCurrentPageShapes().some((shape) => shape.type === 'live-image')) {
|
||||||
return shape.type === 'live-image'
|
editor.createShape<LiveImageShape>({
|
||||||
})
|
type: 'live-image',
|
||||||
|
x: 120,
|
||||||
if (liveImage) {
|
y: 180,
|
||||||
return
|
props: {
|
||||||
|
w: 512,
|
||||||
|
h: 512,
|
||||||
|
name: 'a city skyline',
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.createShape<LiveImageShape>({
|
|
||||||
type: 'live-image',
|
|
||||||
x: 120,
|
|
||||||
y: 180,
|
|
||||||
props: {
|
|
||||||
w: 512,
|
|
||||||
h: 512,
|
|
||||||
name: 'a city skyline',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 { useLiveImage } from '@/hooks/useLiveImage'
|
||||||
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 { FrameHeading } from './FrameHeading'
|
||||||
import image from 'next/image'
|
|
||||||
import { connect } from 'http2'
|
|
||||||
import { useFal } from '@/hooks/useFal'
|
|
||||||
|
|
||||||
// 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,16 +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
|
|
||||||
// moved out in that group
|
|
||||||
|
|
||||||
if (isInGroup) {
|
if (isInGroup) {
|
||||||
this.editor.reparentShapes(shapes, parent.id)
|
this.editor.reparentShapes(shapes, parent.id)
|
||||||
|
|
@ -158,10 +130,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()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,9 +153,9 @@ export class LiveImageShapeUtil extends ShapeUtil<LiveImageShape> {
|
||||||
override component(shape: LiveImageShape) {
|
override component(shape: LiveImageShape) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
useFal(shape.id, {
|
useLiveImage(shape.id, {
|
||||||
debounceTime: 0,
|
debounceTime: 0,
|
||||||
url: 'wss://110602490-lcm-sd15-i2i.gateway.alpha.fal.ai/ws',
|
appId: '110602490-lcm-plexed-sd15-i2i',
|
||||||
})
|
})
|
||||||
|
|
||||||
const bounds = this.editor.getShapeGeometry(shape).bounds
|
const bounds = this.editor.getShapeGeometry(shape).bounds
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -7,31 +8,29 @@ import {
|
||||||
debounce,
|
debounce,
|
||||||
getHashForObject,
|
getHashForObject,
|
||||||
getSvgAsImage,
|
getSvgAsImage,
|
||||||
|
rng,
|
||||||
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 useLiveImage(
|
||||||
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 = 100, 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)
|
||||||
|
|
||||||
const prevHash = useRef<string | null>(null)
|
const prevHash = useRef<string | null>(null)
|
||||||
|
const prevPrompt = useRef<string>('')
|
||||||
|
|
||||||
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,110 +68,80 @@ 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: throttleTime,
|
||||||
// console.log("WebSocket Open");
|
onError: (error) => {
|
||||||
|
console.error(error)
|
||||||
|
},
|
||||||
|
onResult: (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]))
|
||||||
(id) => editor.getShape(id)
|
.filter((id) => id !== shapeId)
|
||||||
) as TLShape[]
|
.map((id) => editor.getShape(id)) as TLShape[]
|
||||||
|
|
||||||
const hash = getHashForObject(shapes)
|
|
||||||
if (hash === prevHash.current) return
|
|
||||||
|
|
||||||
const shape = editor.getShape<LiveImageShape>(shapeId)!
|
const shape = editor.getShape<LiveImageShape>(shapeId)!
|
||||||
|
const hash = getHashForObject(shapes)
|
||||||
|
if (hash === prevHash.current && shape.props.name === prevPrompt.current) return
|
||||||
|
prevHash.current = hash
|
||||||
|
prevPrompt.current = shape.props.name
|
||||||
|
|
||||||
const svg = await editor.getSvg([shape], {
|
const svg = await editor.getSvg([shape], {
|
||||||
background: true,
|
background: true,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
darkMode: editor.user.getIsDarkMode(),
|
darkMode: editor.user.getIsDarkMode(),
|
||||||
})
|
})
|
||||||
|
// We might be stale
|
||||||
|
if (iteration <= finishedIteration.current) return
|
||||||
if (!svg) {
|
if (!svg) {
|
||||||
|
console.error('No SVG')
|
||||||
updateImage('')
|
updateImage('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We might be stale
|
|
||||||
if (iteration <= finishedIteration.current) return
|
|
||||||
|
|
||||||
const image = await getSvgAsImage(svg, editor.environment.isSafari, {
|
const image = await getSvgAsImage(svg, editor.environment.isSafari, {
|
||||||
type: 'png',
|
type: 'png',
|
||||||
quality: 1,
|
quality: 1,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
})
|
})
|
||||||
|
// We might be stale
|
||||||
|
if (iteration <= finishedIteration.current) return
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
console.error('No image')
|
||||||
updateImage('')
|
updateImage('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We might be stale
|
|
||||||
if (iteration <= finishedIteration.current) return
|
|
||||||
|
|
||||||
const prompt = shape.props.name
|
const prompt = shape.props.name
|
||||||
? 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))
|
|
||||||
finishedIteration.current = iteration
|
const random = rng()
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendCurrentData({
|
||||||
|
prompt,
|
||||||
|
image_url: imageDataUri,
|
||||||
|
sync_mode: true,
|
||||||
|
strength: 0.7,
|
||||||
|
seed: Math.abs(random() * 10000), // TODO make this configurable in the UI
|
||||||
|
enable_safety_checks: false,
|
||||||
|
})
|
||||||
|
finishedIteration.current = iteration
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrawingChange = debounceTime
|
const onDrawingChange = debounceTime
|
||||||
|
|
@ -184,7 +153,12 @@ export function useFal(
|
||||||
editor.on('update-drawings' as any, onDrawingChange)
|
editor.on('update-drawings' as any, onDrawingChange)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
try {
|
||||||
|
close()
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
editor.off('update-drawings' as any, onDrawingChange)
|
editor.off('update-drawings' as any, onDrawingChange)
|
||||||
}
|
}
|
||||||
}, [editor, shapeId, throttleTime, debounceTime, url])
|
}, [editor, shapeId, throttleTime, debounceTime, appId])
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue