mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Single-canvas juggling (#15)
This PR adds a `CanvasProviderElement` to `@storyteller/studio-web` to manage the complexity of maintaining a single concrete `HTMLCanvasElement` for `@storyteller/studio`, while still being able to treat components like the `ViewerElement` as self-contained "views" that behave as expected with regards to CSS styling, DOM placement, etc. Consuming applications should wrap the new `<sts-canvas-provider />` element around some common ancestor of all views into the `@storyteller/studio` app, similar to a React context provider. The `react-frontend` application has been updated to take advantage of the new `CanvasProviderElement` by demonstrating a more realistic use case for the `ViewerElement`. The new demo presents a grid of "thumbnails," one for each object in our hard-coded "object library." Clicking on one of these thumbnails will render a `ViewerElement` for that object in a modal dialog.
This commit is contained in:
@@ -1,5 +1,58 @@
|
||||
.root {
|
||||
position: relative;
|
||||
padding: 64px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
flex: 0 0 352px;
|
||||
width: 352px;
|
||||
height: 352px;
|
||||
background: #444;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #2222227F;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
top: calc(50vh - 360px);
|
||||
left: calc(50vw - 640px);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 16px rgba(0,0,0,0.17),
|
||||
0 16px 48px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #222;
|
||||
pointer-events: none;
|
||||
transition: opacity 66.666ms linear;
|
||||
}
|
||||
|
||||
.controls {
|
||||
@@ -34,13 +87,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
.modalClose {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #222;
|
||||
pointer-events: none;
|
||||
transition: opacity 66.666ms linear;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
border-radius: 50%;
|
||||
appearance: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #2222227F;
|
||||
backdrop-filter: blur(16px);
|
||||
font: unset {
|
||||
family: inherit;
|
||||
weight: 400;
|
||||
size: 40px;
|
||||
}
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #222222AA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type SceneStateEvent, SceneState } from "@storyteller/studio";
|
||||
import { FormEvent, useState } from "react";
|
||||
|
||||
import StudioViewer from "./studio-viewer";
|
||||
import StudioCanvasProvider from "./studio-canvas-provider";
|
||||
|
||||
import style from "./app.module.scss";
|
||||
|
||||
@@ -79,14 +80,11 @@ const SKYBOXES = [
|
||||
];
|
||||
|
||||
export default () => {
|
||||
const [objectId, setObjectId] = useState("desk");
|
||||
const [skyboxId, setSkyboxId] = useState("test_scene");
|
||||
const [objectId, setObjectId] = useState<string|null>(null);
|
||||
const [skyboxId, setSkyboxId] = useState("meadow_4k");
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const onObjectSelect = (event: FormEvent<HTMLSelectElement>) => {
|
||||
setObjectId((event.target as HTMLSelectElement).value)
|
||||
}
|
||||
|
||||
const onSkyboxSelect = (event: FormEvent<HTMLSelectElement>) => {
|
||||
setSkyboxId((event.target as HTMLSelectElement).value)
|
||||
}
|
||||
@@ -96,62 +94,75 @@ export default () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.root}>
|
||||
<StudioViewer
|
||||
objectId={objectId}
|
||||
skyboxId={skyboxId}
|
||||
onSceneStateChange={onSceneStateChange}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={style.loading}
|
||||
style={{
|
||||
opacity: loading ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<h1>Loading...</h1>
|
||||
<StudioCanvasProvider>
|
||||
<div className={style.root}>
|
||||
{OBJECTS.map(({ id, label }) => (
|
||||
<div
|
||||
className={style.thumbnail}
|
||||
key={id}
|
||||
onClick={() => {
|
||||
if (id !== objectId) {
|
||||
setLoading(true);
|
||||
setObjectId(id);
|
||||
}
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<h3>{label}</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={style.controls}>
|
||||
<label className={style.control}>
|
||||
<span className={style.label}>Object</span>
|
||||
<select
|
||||
className={style.input}
|
||||
name="object"
|
||||
value={objectId}
|
||||
disabled={loading}
|
||||
onChange={onObjectSelect}
|
||||
{modalOpen && !!objectId && <>
|
||||
<div
|
||||
className={style.modalOverlay}
|
||||
onClick={() => setModalOpen(false)}
|
||||
/>
|
||||
<div className={style.modal}>
|
||||
<StudioViewer
|
||||
objectId={objectId}
|
||||
skyboxId={skyboxId}
|
||||
onSceneStateChange={onSceneStateChange}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={style.loading}
|
||||
style={{
|
||||
opacity: loading ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{OBJECTS.map(({ id, label }) => (
|
||||
<option
|
||||
key={id}
|
||||
value={id}
|
||||
<h1>Loading...</h1>
|
||||
</div>
|
||||
|
||||
<div className={style.controls}>
|
||||
<label className={style.control}>
|
||||
<span className={style.label}>Skybox</span>
|
||||
<select
|
||||
className={style.input}
|
||||
name="skybox"
|
||||
value={skyboxId}
|
||||
disabled={loading}
|
||||
onChange={onSkyboxSelect}
|
||||
>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className={style.control}>
|
||||
<span className={style.label}>Skybox</span>
|
||||
<select
|
||||
className={style.input}
|
||||
name="skybox"
|
||||
value={skyboxId}
|
||||
disabled={loading}
|
||||
onChange={onSkyboxSelect}
|
||||
{SKYBOXES.map(({ id, label }) => (
|
||||
<option
|
||||
key={id}
|
||||
value={id}
|
||||
>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button className={style.modalClose}
|
||||
onClick={() => setModalOpen(false)}
|
||||
>
|
||||
{SKYBOXES.map(({ id, label }) => (
|
||||
<option
|
||||
key={id}
|
||||
value={id}
|
||||
>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</>}
|
||||
</StudioCanvasProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CanvasProviderElement } from "@storyteller/studio-web/canvas-provider";
|
||||
import "@storyteller/studio-web/canvas-provider";
|
||||
import React, { ReactNode, forwardRef } from "react";
|
||||
|
||||
export interface Props extends React.HTMLProps<CanvasProviderElement> {
|
||||
children?: ReactNode | ReactNode[];
|
||||
}
|
||||
|
||||
export default forwardRef<CanvasProviderElement, Props>(
|
||||
({ children, ...props }, ref) => (
|
||||
<sts-canvas-provider {...props} ref={ref}>
|
||||
{children}
|
||||
</sts-canvas-provider>
|
||||
)
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { SceneStateEvent } from "@storyteller/studio";
|
||||
import { ViewerElement } from "@storyteller/studio-web/viewer";
|
||||
import "@storyteller/studio-web/viewer";
|
||||
import { MutableRefObject, forwardRef, useEffect, useRef } from "react";
|
||||
import React, { MutableRefObject, forwardRef, useEffect, useRef } from "react";
|
||||
|
||||
export interface Props {
|
||||
export interface Props extends React.HTMLProps<ViewerElement> {
|
||||
objectId: string;
|
||||
skyboxId: string;
|
||||
onSceneStateChange?(event: SceneStateEvent): void;
|
||||
@@ -13,6 +13,7 @@ export default forwardRef<ViewerElement, Props>(({
|
||||
objectId,
|
||||
skyboxId,
|
||||
onSceneStateChange,
|
||||
...props
|
||||
}, ref) => {
|
||||
const viewerRef = useRef<ViewerElement>(null);
|
||||
|
||||
@@ -38,6 +39,7 @@ export default forwardRef<ViewerElement, Props>(({
|
||||
|
||||
return (
|
||||
<sts-viewer
|
||||
{...props}
|
||||
ref={refCallback}
|
||||
objectId={objectId}
|
||||
skyboxId={skyboxId}
|
||||
|
||||
@@ -2,7 +2,6 @@ html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: "Fira Sans", sans-serif;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -1,4 +1,5 @@
|
||||
import { ViewerElement } from "@storyteller/studio-web";
|
||||
import { CanvasProviderElement } from "@storyteller/studio-web/canvas-provider";
|
||||
|
||||
declare global {
|
||||
export namespace JSX {
|
||||
@@ -9,6 +10,10 @@ declare global {
|
||||
React.HTMLAttributes<ViewerElement>,
|
||||
ViewerElement,
|
||||
>;
|
||||
"sts-canvas-provider": React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<CanvasProviderElement>,
|
||||
CanvasProviderElement,
|
||||
>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user