Spaces:
Runtime error
Runtime error
Commit
·
40fde09
1
Parent(s):
f1f03f6
working on the new game engine
Browse files- src/app/engines.ts +59 -11
- src/app/games/city.ts +1 -0
- src/app/games/doom.ts +1 -0
- src/app/games/dungeon.ts +1 -0
- src/app/games/enchanters.ts +2 -1
- src/app/games/pirates.ts +1 -0
- src/app/games/types.ts +2 -0
- src/app/games/vernian.ts +3 -2
- src/app/main.tsx +84 -29
- src/app/render.ts +81 -9
- src/app/types.ts +10 -0
- src/components/business/cartesian-image.tsx +47 -0
- src/components/business/cartesian-video.tsx +50 -0
- src/components/business/renderer.tsx +43 -49
- src/components/business/spherical-image.tsx +62 -0
- src/components/business/types.ts +3 -0
src/app/engines.ts
CHANGED
@@ -1,28 +1,76 @@
|
|
1 |
|
2 |
-
export type EngineType =
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
export interface Engine {
|
5 |
type: EngineType
|
6 |
label: string
|
7 |
modelName: string
|
8 |
modelUrl: string
|
|
|
|
|
9 |
}
|
10 |
|
11 |
export const engines: Record<string, Engine> = {
|
12 |
-
|
13 |
-
type: "
|
14 |
-
label: "
|
15 |
-
|
|
|
|
|
16 |
modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
|
17 |
},
|
18 |
-
|
19 |
-
type: "
|
20 |
-
label: "
|
21 |
-
|
|
|
|
|
22 |
modelUrl: "https://huggingface.co/cerspense/zeroscope_v2_576w",
|
23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
}
|
25 |
|
26 |
-
export const defaultEngine: EngineType = "
|
27 |
|
28 |
export const getEngine = (type?: EngineType): Engine => engines[type || defaultEngine] || engines[defaultEngine]
|
|
|
1 |
|
2 |
+
export type EngineType =
|
3 |
+
| "cartesian_image"
|
4 |
+
| "cartesian_video"
|
5 |
+
| "spherical_image"
|
6 |
+
| "spherical_video"
|
7 |
+
| "spherical_stereogram"
|
8 |
+
| "spherical_stereovideo"
|
9 |
|
10 |
export interface Engine {
|
11 |
type: EngineType
|
12 |
label: string
|
13 |
modelName: string
|
14 |
modelUrl: string
|
15 |
+
visible: boolean
|
16 |
+
enabled: boolean
|
17 |
}
|
18 |
|
19 |
export const engines: Record<string, Engine> = {
|
20 |
+
cartesian_image: {
|
21 |
+
type: "cartesian_image",
|
22 |
+
label: "Cartesian image",
|
23 |
+
visible: true,
|
24 |
+
enabled: true,
|
25 |
+
modelName: "SDXL",
|
26 |
modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
|
27 |
},
|
28 |
+
cartesian_video: {
|
29 |
+
type: "cartesian_video",
|
30 |
+
label: "Cartesian video",
|
31 |
+
visible: false,
|
32 |
+
enabled: false,
|
33 |
+
modelName: "Zeroscope",
|
34 |
modelUrl: "https://huggingface.co/cerspense/zeroscope_v2_576w",
|
35 |
+
},
|
36 |
+
spherical_image: {
|
37 |
+
type: "spherical_image",
|
38 |
+
label: "Spherical image",
|
39 |
+
visible: true,
|
40 |
+
enabled: true,
|
41 |
+
modelName: "SDXL 360",
|
42 |
+
modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
|
43 |
+
},
|
44 |
+
|
45 |
+
spherical_video: {
|
46 |
+
type: "spherical_video",
|
47 |
+
label: "Spherical video",
|
48 |
+
visible: false,
|
49 |
+
enabled: false,
|
50 |
+
modelName: "",
|
51 |
+
modelUrl: "",
|
52 |
+
},
|
53 |
+
|
54 |
+
spherical_stereogram: {
|
55 |
+
type: "spherical_stereogram",
|
56 |
+
label: "Spherical stereogram",
|
57 |
+
visible: false,
|
58 |
+
enabled: false,
|
59 |
+
modelName: "SDXL 360",
|
60 |
+
modelUrl: "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
|
61 |
+
},
|
62 |
+
|
63 |
+
// A.K.A. the Holy Graal
|
64 |
+
spherical_stereovideo: {
|
65 |
+
type: "spherical_stereovideo",
|
66 |
+
label: "Spherical stereovideo",
|
67 |
+
visible: false,
|
68 |
+
enabled: false,
|
69 |
+
modelName: "",
|
70 |
+
modelUrl: "",
|
71 |
+
}
|
72 |
}
|
73 |
|
74 |
+
export const defaultEngine: EngineType = "cartesian_image"
|
75 |
|
76 |
export const getEngine = (type?: EngineType): Engine => engines[type || defaultEngine] || engines[defaultEngine]
|
src/app/games/city.ts
CHANGED
@@ -44,6 +44,7 @@ const initialActionnables = [
|
|
44 |
export const game: Game = {
|
45 |
title: "City",
|
46 |
type: "city",
|
|
|
47 |
className: edu.className,
|
48 |
initialSituation,
|
49 |
initialActionnables,
|
|
|
44 |
export const game: Game = {
|
45 |
title: "City",
|
46 |
type: "city",
|
47 |
+
engine: "cartesian_image",
|
48 |
className: edu.className,
|
49 |
initialSituation,
|
50 |
initialActionnables,
|
src/app/games/doom.ts
CHANGED
@@ -17,6 +17,7 @@ const initialActionnables = [
|
|
17 |
export const game: Game = {
|
18 |
title: "Doom",
|
19 |
type: "doom",
|
|
|
20 |
className: orbitron.className,
|
21 |
initialSituation,
|
22 |
initialActionnables,
|
|
|
17 |
export const game: Game = {
|
18 |
title: "Doom",
|
19 |
type: "doom",
|
20 |
+
engine: "cartesian_image",
|
21 |
className: orbitron.className,
|
22 |
initialSituation,
|
23 |
initialActionnables,
|
src/app/games/dungeon.ts
CHANGED
@@ -48,6 +48,7 @@ const initialActionnables = [
|
|
48 |
export const game: Game = {
|
49 |
title: "Dungeon",
|
50 |
type: "dungeon",
|
|
|
51 |
className: amatic.className,
|
52 |
initialSituation,
|
53 |
initialActionnables,
|
|
|
48 |
export const game: Game = {
|
49 |
title: "Dungeon",
|
50 |
type: "dungeon",
|
51 |
+
engine: "cartesian_image",
|
52 |
className: amatic.className,
|
53 |
initialSituation,
|
54 |
initialActionnables,
|
src/app/games/enchanters.ts
CHANGED
@@ -2,7 +2,7 @@ import { macondo } from "@/lib/fonts"
|
|
2 |
import { Game } from "./types"
|
3 |
|
4 |
const initialSituation = [
|
5 |
-
`looking at a beautiful medieval castle on a lake, with a metallic gate, during golden
|
6 |
].join(", ")
|
7 |
|
8 |
const initialActionnables = [
|
@@ -18,6 +18,7 @@ const initialActionnables = [
|
|
18 |
export const game: Game = {
|
19 |
title: "Enchanters",
|
20 |
type: "enchanters",
|
|
|
21 |
className: macondo.className,
|
22 |
initialSituation,
|
23 |
initialActionnables,
|
|
|
2 |
import { Game } from "./types"
|
3 |
|
4 |
const initialSituation = [
|
5 |
+
`looking at a beautiful medieval castle on a lake, with a metallic gate, during golden hour, surrounded by mountain, with a flying dragon visible afar`,
|
6 |
].join(", ")
|
7 |
|
8 |
const initialActionnables = [
|
|
|
18 |
export const game: Game = {
|
19 |
title: "Enchanters",
|
20 |
type: "enchanters",
|
21 |
+
engine: "spherical_image",
|
22 |
className: macondo.className,
|
23 |
initialSituation,
|
24 |
initialActionnables,
|
src/app/games/pirates.ts
CHANGED
@@ -55,6 +55,7 @@ const initialSituation = [
|
|
55 |
export const game: Game = {
|
56 |
title: "Pirates",
|
57 |
type: "pirates",
|
|
|
58 |
className: lugrasimo.className,
|
59 |
initialSituation,
|
60 |
initialActionnables,
|
|
|
55 |
export const game: Game = {
|
56 |
title: "Pirates",
|
57 |
type: "pirates",
|
58 |
+
engine: "cartesian_image",
|
59 |
className: lugrasimo.className,
|
60 |
initialSituation,
|
61 |
initialActionnables,
|
src/app/games/types.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
|
2 |
export type GameType = "pirates" | "city" | "dungeon" | "doom" | "vernian" | "enchanters"
|
3 |
|
@@ -9,6 +10,7 @@ export interface Scene {
|
|
9 |
export interface Game {
|
10 |
title: string
|
11 |
type: GameType
|
|
|
12 |
className: string
|
13 |
initialSituation: string
|
14 |
initialActionnables: string[]
|
|
|
1 |
+
import { EngineType } from "../engines"
|
2 |
|
3 |
export type GameType = "pirates" | "city" | "dungeon" | "doom" | "vernian" | "enchanters"
|
4 |
|
|
|
10 |
export interface Game {
|
11 |
title: string
|
12 |
type: GameType
|
13 |
+
engine: EngineType
|
14 |
className: string
|
15 |
initialSituation: string
|
16 |
initialActionnables: string[]
|
src/app/games/vernian.ts
CHANGED
@@ -19,17 +19,18 @@ const initialActionnables = [
|
|
19 |
export const game: Game = {
|
20 |
title: "Vernian",
|
21 |
type: "vernian",
|
|
|
22 |
className: imfell.className,
|
23 |
initialSituation,
|
24 |
initialActionnables,
|
25 |
getScenePrompt: (situation?: string) => [
|
26 |
`Screenshot from a videogame`,
|
27 |
-
`steam punk`,
|
28 |
`jules verne architecture and design`,
|
29 |
`mysterious machines and mechanisms`,
|
30 |
`first person`,
|
31 |
-
`unreal engine`,
|
32 |
situation || initialSituation,
|
|
|
33 |
]
|
34 |
}
|
35 |
|
|
|
19 |
export const game: Game = {
|
20 |
title: "Vernian",
|
21 |
type: "vernian",
|
22 |
+
engine: "spherical_image",
|
23 |
className: imfell.className,
|
24 |
initialSituation,
|
25 |
initialActionnables,
|
26 |
getScenePrompt: (situation?: string) => [
|
27 |
`Screenshot from a videogame`,
|
28 |
+
`steam punk decor`,
|
29 |
`jules verne architecture and design`,
|
30 |
`mysterious machines and mechanisms`,
|
31 |
`first person`,
|
|
|
32 |
situation || initialSituation,
|
33 |
+
`unreal engine`,
|
34 |
]
|
35 |
}
|
36 |
|
src/app/main.tsx
CHANGED
@@ -14,7 +14,7 @@ import {
|
|
14 |
SelectValue,
|
15 |
} from "@/components/ui/select"
|
16 |
|
17 |
-
import {
|
18 |
|
19 |
import { RenderedScene } from "./types"
|
20 |
import { Game, GameType } from "./games/types"
|
@@ -24,14 +24,18 @@ import { getDialogue } from "@/app/queries/getDialogue"
|
|
24 |
import { getActionnables } from "@/app/queries/getActionnables"
|
25 |
import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
|
26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
export default function Main() {
|
28 |
const [isPending, startTransition] = useTransition()
|
29 |
-
const [rendered, setRendered] = useState<RenderedScene>(
|
30 |
-
|
31 |
-
error: "",
|
32 |
-
maskBase64: "",
|
33 |
-
segments:[]
|
34 |
-
})
|
35 |
const router = useRouter()
|
36 |
const pathname = usePathname()
|
37 |
const searchParams = useSearchParams()
|
@@ -41,24 +45,22 @@ export default function Main() {
|
|
41 |
const [game, setGame] = useState<Game>(getGame(gameRef.current))
|
42 |
|
43 |
const requestedEngine = (searchParams.get('engine') as EngineType) || defaultEngine
|
44 |
-
// const engineRef = useRef<EngineType>(requestedEngine)
|
45 |
const [engine, setEngine] = useState<Engine>(getEngine(requestedEngine))
|
46 |
|
47 |
const [situation, setSituation] = useState("")
|
48 |
const [scene, setScene] = useState("")
|
49 |
const [dialogue, setDialogue] = useState("")
|
50 |
const [hoveredActionnable, setHoveredActionnable] = useState("")
|
51 |
-
const [isLoading, setLoading] = useState(true)
|
52 |
|
53 |
-
const
|
54 |
-
// console.log(`update view..`)
|
55 |
-
setLoading(true)
|
56 |
|
|
|
|
|
57 |
await startTransition(async () => {
|
58 |
console.log("Rendering a scene for " + game.type)
|
59 |
|
60 |
// console.log(`rendering scene..`)
|
61 |
-
|
62 |
engine,
|
63 |
|
64 |
// SCENE PROMPT
|
@@ -71,30 +73,74 @@ export default function Main() {
|
|
71 |
).slice(0, 6) // too many can slow us down it seems
|
72 |
})
|
73 |
|
|
|
|
|
74 |
// detect if type game type changed while we were busy
|
75 |
if (game?.type !== gameRef?.current) {
|
76 |
console.log("game type changed! aborting..")
|
77 |
return
|
78 |
-
}
|
79 |
|
80 |
-
|
81 |
-
// console.log(`got a new url: ${newRendered.assetUrl}`)
|
82 |
-
setScene(scene)
|
83 |
|
84 |
-
|
85 |
-
setLoading(false)
|
86 |
-
}
|
87 |
})
|
88 |
}
|
89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
useEffect(() => {
|
91 |
loadNextScene()
|
|
|
92 |
}, [])
|
93 |
|
94 |
const handleUserAction = async (actionnable: string) => {
|
95 |
console.log("user actionnable:", actionnable)
|
96 |
|
97 |
-
setLoading(true)
|
98 |
|
99 |
// TODO: ask Llama2 what to do about it
|
100 |
// we need a frame and some actionnables,
|
@@ -128,7 +174,6 @@ export default function Main() {
|
|
128 |
// todo we could also use useEffect
|
129 |
} catch (err) {
|
130 |
console.error(`failed to get one of the mandatory entites: ${err}`)
|
131 |
-
setLoading(false)
|
132 |
}
|
133 |
})
|
134 |
}
|
@@ -138,13 +183,16 @@ export default function Main() {
|
|
138 |
const handleSelectGame = (newGameType: GameType) => {
|
139 |
gameRef.current = newGameType
|
140 |
setGame(getGame(newGameType))
|
|
|
141 |
setRendered({
|
|
|
|
|
142 |
assetUrl: "",
|
143 |
error: "",
|
144 |
maskBase64: "",
|
145 |
segments:[]
|
146 |
})
|
147 |
-
|
148 |
|
149 |
const current = new URLSearchParams(Array.from(searchParams.entries()))
|
150 |
current.set("game", newGameType)
|
@@ -186,7 +234,7 @@ export default function Main() {
|
|
186 |
<div
|
187 |
className="flex flex-col w-full max-w-5xl"
|
188 |
>
|
189 |
-
<div className="flex flex-row w-full justify-between items-center px-2 py-2 border-b-1 border-gray-50 bg-gray-800">
|
190 |
<div className="flex flex-row items-center space-x-3 font-mono">
|
191 |
<label className="flex text-sm">Select a story:</label>
|
192 |
<Select
|
@@ -211,8 +259,10 @@ export default function Main() {
|
|
211 |
<SelectValue className="text-sm" placeholder="Type" />
|
212 |
</SelectTrigger>
|
213 |
<SelectContent>
|
214 |
-
{Object.entries(engines)
|
215 |
-
|
|
|
|
|
216 |
)}
|
217 |
</SelectContent>
|
218 |
</Select>
|
@@ -220,12 +270,17 @@ export default function Main() {
|
|
220 |
</div>
|
221 |
|
222 |
<div className={[
|
223 |
-
"flex flex-col w-full pt-4 space-y-3 px-2",
|
224 |
getGame(gameRef.current).className // apply the game theme
|
225 |
].join(" ")}>
|
226 |
<p className="text-xl">A stable diffusion exploration game. Click on an item to explore a new scene!</p>
|
227 |
<div className="flex flex-row">
|
228 |
-
<div className="text-xl mr-2"
|
|
|
|
|
|
|
|
|
|
|
229 |
{clickables.map((clickable, i) =>
|
230 |
<div key={i} className="flex flex-row text-xl mr-2">
|
231 |
<div className="">{clickable}</div>
|
@@ -237,7 +292,7 @@ export default function Main() {
|
|
237 |
rendered={rendered}
|
238 |
onUserAction={handleUserAction}
|
239 |
onUserHover={setHoveredActionnable}
|
240 |
-
isLoading={
|
241 |
game={game}
|
242 |
engine={engine}
|
243 |
/>
|
|
|
14 |
SelectValue,
|
15 |
} from "@/components/ui/select"
|
16 |
|
17 |
+
import { newRender, getRender } from "./render"
|
18 |
|
19 |
import { RenderedScene } from "./types"
|
20 |
import { Game, GameType } from "./games/types"
|
|
|
24 |
import { getActionnables } from "@/app/queries/getActionnables"
|
25 |
import { Engine, EngineType, defaultEngine, engines, getEngine } from "./engines"
|
26 |
|
27 |
+
const getInitialRenderedScene = (): RenderedScene => ({
|
28 |
+
renderId: "",
|
29 |
+
status: "pending",
|
30 |
+
assetUrl: "",
|
31 |
+
error: "",
|
32 |
+
maskBase64: "",
|
33 |
+
segments: []
|
34 |
+
})
|
35 |
export default function Main() {
|
36 |
const [isPending, startTransition] = useTransition()
|
37 |
+
const [rendered, setRendered] = useState<RenderedScene>(getInitialRenderedScene())
|
38 |
+
const renderedRef = useRef<RenderedScene>()
|
|
|
|
|
|
|
|
|
39 |
const router = useRouter()
|
40 |
const pathname = usePathname()
|
41 |
const searchParams = useSearchParams()
|
|
|
45 |
const [game, setGame] = useState<Game>(getGame(gameRef.current))
|
46 |
|
47 |
const requestedEngine = (searchParams.get('engine') as EngineType) || defaultEngine
|
|
|
48 |
const [engine, setEngine] = useState<Engine>(getEngine(requestedEngine))
|
49 |
|
50 |
const [situation, setSituation] = useState("")
|
51 |
const [scene, setScene] = useState("")
|
52 |
const [dialogue, setDialogue] = useState("")
|
53 |
const [hoveredActionnable, setHoveredActionnable] = useState("")
|
|
|
54 |
|
55 |
+
const loopRef = useRef<any>(null)
|
|
|
|
|
56 |
|
57 |
+
const loadNextScene = async (nextSituation?: string, nextActionnables?: string[]) => {
|
58 |
+
|
59 |
await startTransition(async () => {
|
60 |
console.log("Rendering a scene for " + game.type)
|
61 |
|
62 |
// console.log(`rendering scene..`)
|
63 |
+
renderedRef.current = await newRender({
|
64 |
engine,
|
65 |
|
66 |
// SCENE PROMPT
|
|
|
73 |
).slice(0, 6) // too many can slow us down it seems
|
74 |
})
|
75 |
|
76 |
+
console.log("got the first version of our scene!", renderedRef.current)
|
77 |
+
|
78 |
// detect if type game type changed while we were busy
|
79 |
if (game?.type !== gameRef?.current) {
|
80 |
console.log("game type changed! aborting..")
|
81 |
return
|
82 |
+
}
|
83 |
|
84 |
+
setScene(scene)
|
|
|
|
|
85 |
|
86 |
+
setRendered(renderedRef.current)
|
|
|
|
|
87 |
})
|
88 |
}
|
89 |
|
90 |
+
const checkRenderedLoop = async () => {
|
91 |
+
// console.log("checkRenderedLoop! rendered:", renderedRef.current)
|
92 |
+
clearTimeout(loopRef.current)
|
93 |
+
if (!renderedRef.current?.renderId || renderedRef.current?.status !== "pending") {
|
94 |
+
// console.log("let's try again in a moments")
|
95 |
+
loopRef.current = setTimeout(() => checkRenderedLoop(), 200)
|
96 |
+
return
|
97 |
+
}
|
98 |
+
|
99 |
+
// console.log("checking rendering..")
|
100 |
+
await startTransition(async () => {
|
101 |
+
// console.log(`getting latest updated scene..`)
|
102 |
+
try {
|
103 |
+
if (!renderedRef.current?.renderId) {
|
104 |
+
throw new Error(`missing renderId`)
|
105 |
+
}
|
106 |
+
|
107 |
+
|
108 |
+
// console.log(`calling getRender(${renderedRef.current.renderId})`)
|
109 |
+
const newRendered = await getRender(renderedRef.current.renderId)
|
110 |
+
// console.log(`got latest updated scene:`, renderedRef.current)
|
111 |
+
|
112 |
+
// detect if type game type changed while we were busy
|
113 |
+
if (game?.type !== gameRef?.current) {
|
114 |
+
console.log("game type changed! aborting..")
|
115 |
+
return
|
116 |
+
}
|
117 |
+
|
118 |
+
|
119 |
+
const before = JSON.stringify(renderedRef.current)
|
120 |
+
const after = JSON.stringify(newRendered)
|
121 |
+
|
122 |
+
if (after !== before) {
|
123 |
+
console.log("updating scene..")
|
124 |
+
renderedRef.current = newRendered
|
125 |
+
setRendered(renderedRef.current)
|
126 |
+
}
|
127 |
+
} catch (err) {
|
128 |
+
console.error(err)
|
129 |
+
}
|
130 |
+
|
131 |
+
clearTimeout(loopRef.current)
|
132 |
+
loopRef.current = setTimeout(() => checkRenderedLoop(), 1000)
|
133 |
+
})
|
134 |
+
}
|
135 |
+
|
136 |
useEffect(() => {
|
137 |
loadNextScene()
|
138 |
+
checkRenderedLoop()
|
139 |
}, [])
|
140 |
|
141 |
const handleUserAction = async (actionnable: string) => {
|
142 |
console.log("user actionnable:", actionnable)
|
143 |
|
|
|
144 |
|
145 |
// TODO: ask Llama2 what to do about it
|
146 |
// we need a frame and some actionnables,
|
|
|
174 |
// todo we could also use useEffect
|
175 |
} catch (err) {
|
176 |
console.error(`failed to get one of the mandatory entites: ${err}`)
|
|
|
177 |
}
|
178 |
})
|
179 |
}
|
|
|
183 |
const handleSelectGame = (newGameType: GameType) => {
|
184 |
gameRef.current = newGameType
|
185 |
setGame(getGame(newGameType))
|
186 |
+
/*
|
187 |
setRendered({
|
188 |
+
renderId: "",
|
189 |
+
status: "pending",
|
190 |
assetUrl: "",
|
191 |
error: "",
|
192 |
maskBase64: "",
|
193 |
segments:[]
|
194 |
})
|
195 |
+
*/
|
196 |
|
197 |
const current = new URLSearchParams(Array.from(searchParams.entries()))
|
198 |
current.set("game", newGameType)
|
|
|
234 |
<div
|
235 |
className="flex flex-col w-full max-w-5xl"
|
236 |
>
|
237 |
+
<div className="flex flex-row w-full justify-between items-center px-2 py-2 border-b-1 border-gray-50 dark:border-gray-50 bg-gray-800 dark:bg-gray-800">
|
238 |
<div className="flex flex-row items-center space-x-3 font-mono">
|
239 |
<label className="flex text-sm">Select a story:</label>
|
240 |
<Select
|
|
|
259 |
<SelectValue className="text-sm" placeholder="Type" />
|
260 |
</SelectTrigger>
|
261 |
<SelectContent>
|
262 |
+
{Object.entries(engines)
|
263 |
+
.filter(([_, engine]) => engine.visible)
|
264 |
+
.map(([key, engine]) =>
|
265 |
+
<SelectItem key={key} value={key} disabled={!engine.enabled}>{engine.label} ({engine.modelName})</SelectItem>
|
266 |
)}
|
267 |
</SelectContent>
|
268 |
</Select>
|
|
|
270 |
</div>
|
271 |
|
272 |
<div className={[
|
273 |
+
"flex flex-col w-full pt-4 space-y-3 px-2 text-gray-50 dark:text-gray-50",
|
274 |
getGame(gameRef.current).className // apply the game theme
|
275 |
].join(" ")}>
|
276 |
<p className="text-xl">A stable diffusion exploration game. Click on an item to explore a new scene!</p>
|
277 |
<div className="flex flex-row">
|
278 |
+
<div className="text-xl mr-2">
|
279 |
+
{rendered.segments.length
|
280 |
+
? <span>🔎 Clickable items:</span>
|
281 |
+
: <span>⌛ Loading clickable items..</span>
|
282 |
+
}
|
283 |
+
</div>
|
284 |
{clickables.map((clickable, i) =>
|
285 |
<div key={i} className="flex flex-row text-xl mr-2">
|
286 |
<div className="">{clickable}</div>
|
|
|
292 |
rendered={rendered}
|
293 |
onUserAction={handleUserAction}
|
294 |
onUserHover={setHoveredActionnable}
|
295 |
+
isLoading={rendered.status === "pending"}
|
296 |
game={game}
|
297 |
engine={engine}
|
298 |
/>
|
src/app/render.ts
CHANGED
@@ -9,10 +9,9 @@ import { Engine, EngineType } from "./engines"
|
|
9 |
// so we have to add it ourselves if needed
|
10 |
const apiUrl = process.env.RENDERING_ENGINE_API
|
11 |
|
12 |
-
|
13 |
const cacheDurationInSec = 30 * 60 // 30 minutes
|
14 |
|
15 |
-
export async function
|
16 |
prompt,
|
17 |
actionnables = [],
|
18 |
engine,
|
@@ -30,20 +29,39 @@ export async function render({
|
|
30 |
throw new Error(`cannot call the rendering API without actionnables, aborting..`)
|
31 |
}
|
32 |
|
33 |
-
const nbFrames = engine.type === "
|
34 |
|
35 |
const cacheKey = `render/${JSON.stringify({ prompt, actionnables, nbFrames, type: engine.type })}`
|
36 |
|
37 |
-
return await Gorgon.get(cacheKey, async () => {
|
|
|
38 |
let defaulResult: RenderedScene = {
|
|
|
|
|
39 |
assetUrl: "",
|
40 |
maskBase64: "",
|
41 |
-
error: "",
|
42 |
segments: []
|
43 |
}
|
44 |
|
45 |
try {
|
46 |
-
console.log(`calling ${apiUrl}/render with prompt: ${prompt}`)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
const res = await fetch(`${apiUrl}/render`, {
|
48 |
method: "POST",
|
49 |
headers: {
|
@@ -55,9 +73,11 @@ export async function render({
|
|
55 |
prompt,
|
56 |
// nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
|
57 |
nbFrames, // when nbFrames is 1, we will only generate static images
|
58 |
-
nbSteps: 20,
|
59 |
actionnables,
|
60 |
segmentation: "firstframe", // one day we will remove this param, to make it automatic
|
|
|
|
|
61 |
}),
|
62 |
cache: 'no-store',
|
63 |
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
@@ -79,8 +99,60 @@ export async function render({
|
|
79 |
return response
|
80 |
} catch (err) {
|
81 |
console.error(err)
|
82 |
-
Gorgon.clear(cacheKey)
|
83 |
return defaulResult
|
84 |
}
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
}
|
|
|
9 |
// so we have to add it ourselves if needed
|
10 |
const apiUrl = process.env.RENDERING_ENGINE_API
|
11 |
|
|
|
12 |
const cacheDurationInSec = 30 * 60 // 30 minutes
|
13 |
|
14 |
+
export async function newRender({
|
15 |
prompt,
|
16 |
actionnables = [],
|
17 |
engine,
|
|
|
29 |
throw new Error(`cannot call the rendering API without actionnables, aborting..`)
|
30 |
}
|
31 |
|
32 |
+
const nbFrames = engine.type === "cartesian_video" ? 8 : 1
|
33 |
|
34 |
const cacheKey = `render/${JSON.stringify({ prompt, actionnables, nbFrames, type: engine.type })}`
|
35 |
|
36 |
+
// return await Gorgon.get(cacheKey, async () => {
|
37 |
+
|
38 |
let defaulResult: RenderedScene = {
|
39 |
+
renderId: "",
|
40 |
+
status: "error",
|
41 |
assetUrl: "",
|
42 |
maskBase64: "",
|
43 |
+
error: "failed to fetch the data",
|
44 |
segments: []
|
45 |
}
|
46 |
|
47 |
try {
|
48 |
+
// console.log(`calling POST ${apiUrl}/render with prompt: ${prompt}`)
|
49 |
+
|
50 |
+
const isForVideo = nbFrames > 1
|
51 |
+
|
52 |
+
|
53 |
+
console.log("REQUEST:", JSON.stringify({
|
54 |
+
prompt,
|
55 |
+
// nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
|
56 |
+
nbFrames, // when nbFrames is 1, we will only generate static images
|
57 |
+
nbSteps: isForVideo ? 20 : 30, // 20 = fast, 30 = better, 50 = best
|
58 |
+
actionnables,
|
59 |
+
segmentation: "firstframe", // one day we will remove this param, to make it automatic
|
60 |
+
width: isForVideo ? 576 : 1024,
|
61 |
+
height: isForVideo ? 320 : 512,
|
62 |
+
}))
|
63 |
+
|
64 |
+
|
65 |
const res = await fetch(`${apiUrl}/render`, {
|
66 |
method: "POST",
|
67 |
headers: {
|
|
|
73 |
prompt,
|
74 |
// nbFrames: 8 and nbSteps: 15 --> ~10 sec generation
|
75 |
nbFrames, // when nbFrames is 1, we will only generate static images
|
76 |
+
nbSteps: isForVideo ? 12 : 30, // 20 = fast, 30 = better, 50 = best
|
77 |
actionnables,
|
78 |
segmentation: "firstframe", // one day we will remove this param, to make it automatic
|
79 |
+
width: isForVideo ? 576 : 1024,
|
80 |
+
height: isForVideo ? 320 : 512,
|
81 |
}),
|
82 |
cache: 'no-store',
|
83 |
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
|
|
99 |
return response
|
100 |
} catch (err) {
|
101 |
console.error(err)
|
102 |
+
// Gorgon.clear(cacheKey)
|
103 |
return defaulResult
|
104 |
}
|
105 |
+
|
106 |
+
// }, cacheDurationInSec * 1000)
|
107 |
+
}
|
108 |
+
|
109 |
+
export async function getRender(renderId: string) {
|
110 |
+
if (!renderId) {
|
111 |
+
console.error(`cannot call the rendering API without a renderId, aborting..`)
|
112 |
+
throw new Error(`cannot call the rendering API without a renderId, aborting..`)
|
113 |
+
}
|
114 |
+
|
115 |
+
let defaulResult: RenderedScene = {
|
116 |
+
renderId: "",
|
117 |
+
status: "error",
|
118 |
+
assetUrl: "",
|
119 |
+
maskBase64: "",
|
120 |
+
error: "failed to fetch the data",
|
121 |
+
segments: []
|
122 |
+
}
|
123 |
+
|
124 |
+
try {
|
125 |
+
console.log(`calling GET ${apiUrl}/render with renderId: ${renderId}`)
|
126 |
+
const res = await fetch(`${apiUrl}/render/${renderId}`, {
|
127 |
+
method: "GET",
|
128 |
+
headers: {
|
129 |
+
Accept: "application/json",
|
130 |
+
"Content-Type": "application/json",
|
131 |
+
// Authorization: `Bearer ${process.env.VC_SECRET_ACCESS_TOKEN}`,
|
132 |
+
},
|
133 |
+
cache: 'no-store',
|
134 |
+
// we can also use this (see https://vercel.com/blog/vercel-cache-api-nextjs-cache)
|
135 |
+
// next: { revalidate: 1 }
|
136 |
+
})
|
137 |
+
|
138 |
+
// console.log("res:", res)
|
139 |
+
// The return value is *not* serialized
|
140 |
+
// You can return Date, Map, Set, etc.
|
141 |
+
|
142 |
+
// Recommendation: handle errors
|
143 |
+
if (res.status !== 200) {
|
144 |
+
// This will activate the closest `error.js` Error Boundary
|
145 |
+
throw new Error('Failed to fetch data')
|
146 |
+
}
|
147 |
+
|
148 |
+
const response = (await res.json()) as RenderedScene
|
149 |
+
// console.log("response:", response)
|
150 |
+
return response
|
151 |
+
} catch (err) {
|
152 |
+
console.error(err)
|
153 |
+
// Gorgon.clear(cacheKey)
|
154 |
+
return defaulResult
|
155 |
+
}
|
156 |
+
|
157 |
+
// }, cacheDurationInSec * 1000)
|
158 |
}
|
src/app/types.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
export interface RenderRequest {
|
2 |
prompt: string
|
3 |
|
@@ -21,6 +23,11 @@ export interface RenderRequest {
|
|
21 |
nbSteps: number // min: 1, max: 50
|
22 |
|
23 |
seed: number
|
|
|
|
|
|
|
|
|
|
|
24 |
}
|
25 |
|
26 |
export interface ImageSegment {
|
@@ -31,7 +38,10 @@ export interface ImageSegment {
|
|
31 |
score: number
|
32 |
}
|
33 |
|
|
|
34 |
export interface RenderedScene {
|
|
|
|
|
35 |
assetUrl: string
|
36 |
error: string
|
37 |
maskBase64: string
|
|
|
1 |
+
export type ProjectionMode = 'cartesian' | 'spherical'
|
2 |
+
|
3 |
export interface RenderRequest {
|
4 |
prompt: string
|
5 |
|
|
|
23 |
nbSteps: number // min: 1, max: 50
|
24 |
|
25 |
seed: number
|
26 |
+
|
27 |
+
width: number // fixed at 1024 for now
|
28 |
+
height: number // fixed at 512 for now
|
29 |
+
|
30 |
+
projection: ProjectionMode
|
31 |
}
|
32 |
|
33 |
export interface ImageSegment {
|
|
|
38 |
score: number
|
39 |
}
|
40 |
|
41 |
+
export type RenderedSceneStatus = 'pending' | 'completed' | 'error'
|
42 |
export interface RenderedScene {
|
43 |
+
renderId: string
|
44 |
+
status: RenderedSceneStatus
|
45 |
assetUrl: string
|
46 |
error: string
|
47 |
maskBase64: string
|
src/components/business/cartesian-image.tsx
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ForwardedRef, forwardRef } from "react"
|
2 |
+
|
3 |
+
import { SceneEventHandler } from "./types"
|
4 |
+
|
5 |
+
export const CartesianImage = forwardRef(({
|
6 |
+
src,
|
7 |
+
width,
|
8 |
+
height,
|
9 |
+
onEvent,
|
10 |
+
className,
|
11 |
+
}: {
|
12 |
+
src: string
|
13 |
+
width: number | string
|
14 |
+
height: number | string
|
15 |
+
onEvent: SceneEventHandler
|
16 |
+
className?: string
|
17 |
+
}, ref: ForwardedRef<HTMLImageElement>) => {
|
18 |
+
|
19 |
+
const handleEvent = (event: React.MouseEvent<HTMLImageElement, MouseEvent>, isClick: boolean) => {
|
20 |
+
|
21 |
+
const element = ((ref as any)?.current) as HTMLImageElement
|
22 |
+
|
23 |
+
if (!element) {
|
24 |
+
console.log("element isn't ready")
|
25 |
+
return
|
26 |
+
}
|
27 |
+
|
28 |
+
const boundingRect = element.getBoundingClientRect()
|
29 |
+
const x = event.clientX - boundingRect.left
|
30 |
+
const y = event.clientY - boundingRect.top
|
31 |
+
|
32 |
+
const eventType = isClick ? "click" : "hover"
|
33 |
+
onEvent(eventType, x, y)
|
34 |
+
}
|
35 |
+
|
36 |
+
return (
|
37 |
+
<img
|
38 |
+
src={src}
|
39 |
+
ref={ref}
|
40 |
+
width={width}
|
41 |
+
height={height}
|
42 |
+
className={className}
|
43 |
+
onMouseUp={(event) => handleEvent(event, true)}
|
44 |
+
onMouseMove={(event) => handleEvent(event, false)}
|
45 |
+
/>
|
46 |
+
)
|
47 |
+
})
|
src/components/business/cartesian-video.tsx
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ForwardedRef, forwardRef } from "react"
|
2 |
+
|
3 |
+
import { SceneEventHandler } from "./types"
|
4 |
+
|
5 |
+
export const CartesianVideo = forwardRef(({
|
6 |
+
src,
|
7 |
+
width,
|
8 |
+
height,
|
9 |
+
onEvent,
|
10 |
+
className,
|
11 |
+
}: {
|
12 |
+
src: string
|
13 |
+
width: number | string
|
14 |
+
height: number | string
|
15 |
+
onEvent: SceneEventHandler
|
16 |
+
className?: string
|
17 |
+
}, ref: ForwardedRef<HTMLVideoElement>) => {
|
18 |
+
|
19 |
+
const handleEvent = (event: React.MouseEvent<HTMLVideoElement, MouseEvent>, isClick: boolean) => {
|
20 |
+
|
21 |
+
const element = ((ref as any)?.current) as HTMLVideoElement
|
22 |
+
|
23 |
+
if (!element) {
|
24 |
+
console.log("element isn't ready")
|
25 |
+
return
|
26 |
+
}
|
27 |
+
|
28 |
+
const boundingRect = element.getBoundingClientRect()
|
29 |
+
const x = event.clientX - boundingRect.left
|
30 |
+
const y = event.clientY - boundingRect.top
|
31 |
+
|
32 |
+
const eventType = isClick ? "click" : "hover"
|
33 |
+
onEvent(eventType, x, y)
|
34 |
+
}
|
35 |
+
|
36 |
+
return (
|
37 |
+
<video
|
38 |
+
src={src}
|
39 |
+
ref={ref}
|
40 |
+
muted
|
41 |
+
autoPlay
|
42 |
+
loop
|
43 |
+
width={width}
|
44 |
+
height={height}
|
45 |
+
className={className}
|
46 |
+
onMouseUp={(event) => handleEvent(event, true)}
|
47 |
+
onMouseMove={(event) => handleEvent(event, false)}
|
48 |
+
/>
|
49 |
+
)
|
50 |
+
})
|
src/components/business/renderer.tsx
CHANGED
@@ -4,6 +4,10 @@ import { ImageSegment, RenderedScene } from "@/app/types"
|
|
4 |
import { ProgressBar } from "../misc/progress"
|
5 |
import { Game } from "@/app/games/types"
|
6 |
import { Engine, EngineType } from "@/app/engines"
|
|
|
|
|
|
|
|
|
7 |
|
8 |
export const Renderer = ({
|
9 |
rendered: {
|
@@ -106,26 +110,22 @@ export const Renderer = ({
|
|
106 |
return closestSegment;
|
107 |
}
|
108 |
|
109 |
-
const handleMouseEvent = async (
|
110 |
if (!contextRef.current) return; // Return early if mask image has not been loaded yet
|
111 |
|
112 |
if (isLoading) {
|
113 |
// we ignore all user interactions
|
114 |
-
return
|
115 |
}
|
116 |
|
117 |
// sometimes we generate an image, but the segmentation fails
|
118 |
// so if we click anywhere bug there are no segments,
|
119 |
// we inform the rest of the app by passing nothing
|
120 |
-
if (
|
121 |
onUserAction("nothing, to trigger a scene reload")
|
122 |
return
|
123 |
}
|
124 |
|
125 |
-
const boundingRect = imgRef.current!.getBoundingClientRect();
|
126 |
-
const x = event.clientX - boundingRect.left;
|
127 |
-
const y = event.clientY - boundingRect.top;
|
128 |
-
|
129 |
const newSegment = getSegmentAt(x, y)
|
130 |
|
131 |
if (actionnable !== newSegment.label) {
|
@@ -139,7 +139,7 @@ export const Renderer = ({
|
|
139 |
setActionnable(newSegment.label)
|
140 |
}
|
141 |
|
142 |
-
if (
|
143 |
if (!newSegment.label) {
|
144 |
return
|
145 |
}
|
@@ -212,49 +212,43 @@ export const Renderer = ({
|
|
212 |
*/
|
213 |
|
214 |
return (
|
215 |
-
<div className=
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
|
|
|
|
|
|
|
|
221 |
{!assetUrl ?
|
222 |
null
|
223 |
-
: engine.type === "
|
224 |
-
? <
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
height="512px"
|
249 |
-
className={
|
250 |
-
[
|
251 |
-
// "absolute top-0 left-0",
|
252 |
-
actionnable && !isLoading ? "cursor-pointer" : ""
|
253 |
-
].join(" ")
|
254 |
-
}
|
255 |
-
onMouseDown={(event) => handleMouseEvent(event, true)}
|
256 |
-
onMouseMove={handleMouseEvent}
|
257 |
-
/>}
|
258 |
</div>
|
259 |
|
260 |
{isLoading
|
|
|
4 |
import { ProgressBar } from "../misc/progress"
|
5 |
import { Game } from "@/app/games/types"
|
6 |
import { Engine, EngineType } from "@/app/engines"
|
7 |
+
import { CartesianImage } from "./cartesian-image"
|
8 |
+
import { SceneEventHandler, SceneEventType } from "./types"
|
9 |
+
import { CartesianVideo } from "./cartesian-video"
|
10 |
+
import { SphericalImage } from "./spherical-image"
|
11 |
|
12 |
export const Renderer = ({
|
13 |
rendered: {
|
|
|
110 |
return closestSegment;
|
111 |
}
|
112 |
|
113 |
+
const handleMouseEvent: SceneEventHandler = async (type: SceneEventType, x: number, y: number) => {
|
114 |
if (!contextRef.current) return; // Return early if mask image has not been loaded yet
|
115 |
|
116 |
if (isLoading) {
|
117 |
// we ignore all user interactions
|
118 |
+
return
|
119 |
}
|
120 |
|
121 |
// sometimes we generate an image, but the segmentation fails
|
122 |
// so if we click anywhere bug there are no segments,
|
123 |
// we inform the rest of the app by passing nothing
|
124 |
+
if (type === "click" && segments.length == 0) {
|
125 |
onUserAction("nothing, to trigger a scene reload")
|
126 |
return
|
127 |
}
|
128 |
|
|
|
|
|
|
|
|
|
129 |
const newSegment = getSegmentAt(x, y)
|
130 |
|
131 |
if (actionnable !== newSegment.label) {
|
|
|
139 |
setActionnable(newSegment.label)
|
140 |
}
|
141 |
|
142 |
+
if (type === "click") {
|
143 |
if (!newSegment.label) {
|
144 |
return
|
145 |
}
|
|
|
212 |
*/
|
213 |
|
214 |
return (
|
215 |
+
<div className="w-full py-8">
|
216 |
+
<div
|
217 |
+
className={[
|
218 |
+
"relative w-full h-[800px] border-2 border-gray-50 rounded-xl overflow-hidden",
|
219 |
+
isLoading
|
220 |
+
? "cursor-wait"
|
221 |
+
: actionnable
|
222 |
+
? "cursor-pointer"
|
223 |
+
: ""
|
224 |
+
].join(" ")}>
|
225 |
{!assetUrl ?
|
226 |
null
|
227 |
+
: engine.type === "cartesian_video"
|
228 |
+
? <CartesianVideo
|
229 |
+
src={assetUrl}
|
230 |
+
ref={imgRef as any}
|
231 |
+
width="1024px"
|
232 |
+
height="512px"
|
233 |
+
onEvent={handleMouseEvent}
|
234 |
+
/>
|
235 |
+
: engine.type === "spherical_image"
|
236 |
+
? <SphericalImage
|
237 |
+
src={assetUrl}
|
238 |
+
ref={imgRef as any}
|
239 |
+
width="1024px"
|
240 |
+
height="512px"
|
241 |
+
onEvent={handleMouseEvent}
|
242 |
+
/>
|
243 |
+
: <CartesianImage
|
244 |
+
src={assetUrl}
|
245 |
+
ref={imgRef as any}
|
246 |
+
width="1024px"
|
247 |
+
height="512px"
|
248 |
+
onEvent={handleMouseEvent}
|
249 |
+
/>
|
250 |
+
}
|
251 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
252 |
</div>
|
253 |
|
254 |
{isLoading
|
src/components/business/spherical-image.tsx
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ForwardedRef, forwardRef } from "react"
|
2 |
+
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
|
3 |
+
// import { CubemapAdapter } from "react-photo-sphere-viewer"
|
4 |
+
|
5 |
+
import { SceneEventHandler } from "./types"
|
6 |
+
|
7 |
+
export const SphericalImage = forwardRef(({
|
8 |
+
src,
|
9 |
+
width,
|
10 |
+
height,
|
11 |
+
onEvent,
|
12 |
+
className,
|
13 |
+
}: {
|
14 |
+
src: string
|
15 |
+
width: number | string
|
16 |
+
height: number | string
|
17 |
+
onEvent: SceneEventHandler
|
18 |
+
className?: string
|
19 |
+
}, ref: ForwardedRef<HTMLImageElement>) => {
|
20 |
+
|
21 |
+
|
22 |
+
return (
|
23 |
+
<ReactPhotoSphereViewer
|
24 |
+
src={src}
|
25 |
+
// ref={ref}
|
26 |
+
container=""
|
27 |
+
containerClass={className}
|
28 |
+
//
|
29 |
+
height="800px"
|
30 |
+
// height={'100vh'}
|
31 |
+
width="100%"
|
32 |
+
|
33 |
+
defaultZoomLvl={1}
|
34 |
+
|
35 |
+
onClick={(data, instance) => {
|
36 |
+
console.log("on click:")
|
37 |
+
const position = data.target.getPosition()
|
38 |
+
console.log("position:", position)
|
39 |
+
}}
|
40 |
+
|
41 |
+
onReady={(instance) => {
|
42 |
+
console.log("spherical image display is ready")
|
43 |
+
/*
|
44 |
+
const markersPlugs = instance.getPlugin(MarkersPlugin);
|
45 |
+
if (!markersPlugs)
|
46 |
+
return;
|
47 |
+
markersPlugs.addMarker({
|
48 |
+
id: "imageLayer2",
|
49 |
+
imageLayer: "drone.png",
|
50 |
+
size: { width: 220, height: 220 },
|
51 |
+
position: { yaw: '130.5deg', pitch: '-0.1deg' },
|
52 |
+
tooltip: "Image embedded in the scene"
|
53 |
+
});
|
54 |
+
markersPlugs.addEventListener("select-marker", () => {
|
55 |
+
console.log("asd");
|
56 |
+
});
|
57 |
+
*/
|
58 |
+
}}
|
59 |
+
|
60 |
+
/>
|
61 |
+
)
|
62 |
+
})
|
src/components/business/types.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
export type SceneEventType = 'hover' | 'click' | 'back'
|
2 |
+
|
3 |
+
export type SceneEventHandler = (type: SceneEventType, x: number, y: number) => Promise<void>
|