mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-18 07:35:19 +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:
@@ -0,0 +1,3 @@
|
||||
::slotted([slot="canvas"]) {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Token, UniqueToken, provide } from "@storyteller/framework";
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import styles from "./canvas-provider.element.scss?inline";
|
||||
|
||||
export interface CanvasProvider {
|
||||
readonly host: HTMLElement;
|
||||
readonly canvasId: string;
|
||||
readonly canvas: HTMLCanvasElement | undefined;
|
||||
|
||||
/**
|
||||
* Remove the canvas element from the provider host and return it. This can
|
||||
* be used to temporarily move the canvas element to a different location in
|
||||
* the DOM tree (e.g., for displaying in a modal dialog).
|
||||
*
|
||||
* `returnCanvas` must be called to restore the canvas element to its
|
||||
* original location before the canvas's new parent element is removed from
|
||||
* the document. If the canvas element is entirely absent from the document
|
||||
* at any point after initialization (i.e., if
|
||||
* `document.getElementById(canvasId)` returns `null`), the
|
||||
* `@storyteller/studio` app will panic.
|
||||
*/
|
||||
takeCanvas(): HTMLCanvasElement | undefined;
|
||||
|
||||
/**
|
||||
* Return the canvas element to the provider after removing it via
|
||||
* `takeCanvas`.
|
||||
*
|
||||
* When `takeCanvas` is used to move the canvas element to a new location,
|
||||
* this method must be called before the canvas's new parent is removed from
|
||||
* the DOM tree, to ensure that the canvas element is always accessible to
|
||||
* `@storyteller/studio`.
|
||||
*/
|
||||
returnCanvas(canvas: HTMLCanvasElement): void;
|
||||
}
|
||||
|
||||
export const CANVAS_PROVIDER: Token<CanvasProvider> = new UniqueToken("CanvasProvider");
|
||||
|
||||
@customElement("sts-canvas-provider")
|
||||
@provide(CANVAS_PROVIDER)
|
||||
export class CanvasProviderElement
|
||||
extends LitElement
|
||||
implements CanvasProvider
|
||||
{
|
||||
static override styles = unsafeCSS(styles);
|
||||
|
||||
get host() { return this; }
|
||||
get canvasId() { return this.#canvasId; }
|
||||
get canvas(): HTMLCanvasElement | undefined { return this.#canvas; }
|
||||
|
||||
#canvasId = "studio_canvas";
|
||||
#canvas?: HTMLCanvasElement;
|
||||
|
||||
takeCanvas(): HTMLCanvasElement | undefined {
|
||||
if (!this.#canvas) return;
|
||||
|
||||
const canvas = this.removeChild(this.#canvas);
|
||||
this.#canvas = undefined;
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
returnCanvas(canvas: HTMLCanvasElement) {
|
||||
this.appendChild(canvas);
|
||||
this.#canvas = canvas;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
this.#canvas ??= this.#appendLightDomCanvas();
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<slot></slot>
|
||||
<slot name="canvas"></slot>
|
||||
`;
|
||||
|
||||
#appendLightDomCanvas(): HTMLCanvasElement {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.id = this.#canvasId;
|
||||
canvas.slot = "canvas";
|
||||
|
||||
this.appendChild(canvas);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface HTMLElementTagNameMap {
|
||||
"sts-canvas-provider": CanvasProviderElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::slotted([slot="canvas"]) {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as studio from "@storyteller/studio";
|
||||
import { ViewportSize } from "@storyteller/studio";
|
||||
import { inject } from "@storyteller/framework";
|
||||
import { LitElement, PropertyValues, html, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { CANVAS_PROVIDER, CanvasProvider } from "./canvas-provider.element";
|
||||
|
||||
import styles from "./viewer.element.scss?inline";
|
||||
import { ViewportSize } from "@storyteller/studio";
|
||||
|
||||
@customElement("sts-viewer")
|
||||
export class ViewerElement extends LitElement {
|
||||
@@ -12,8 +15,32 @@ export class ViewerElement extends LitElement {
|
||||
@property() objectId?: string;
|
||||
@property() skyboxId?: string;
|
||||
|
||||
protected override createRenderRoot(): Element | ShadowRoot {
|
||||
return this;
|
||||
@inject(CANVAS_PROVIDER)
|
||||
_canvasProvider?: CanvasProvider;
|
||||
|
||||
#canvas?: HTMLCanvasElement;
|
||||
#resizeObserver?: ResizeObserver;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));
|
||||
this.#resizeObserver.observe(this);
|
||||
|
||||
if (!this._canvasProvider)
|
||||
throw new Error(`No provider found for token: ${CANVAS_PROVIDER}`);
|
||||
|
||||
if ((this.#canvas = this._canvasProvider.takeCanvas()))
|
||||
this.appendChild(this.#canvas);
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
if (this.#canvas)
|
||||
this._canvasProvider?.returnCanvas(this.removeChild(this.#canvas));
|
||||
|
||||
this.#resizeObserver?.disconnect();
|
||||
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected override updated(changes: PropertyValues<this>): void {
|
||||
@@ -28,17 +55,26 @@ export class ViewerElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<slot name="canvas"></slot>
|
||||
`;
|
||||
|
||||
#onResize(): void {
|
||||
const { width, height } = this.getBoundingClientRect();
|
||||
studio.resize(new ViewportSize(width, height));
|
||||
}
|
||||
|
||||
#startViewer(): void {
|
||||
if (!this.#canvas) return;
|
||||
if (!this.objectId) return;
|
||||
if (!this.skyboxId) return;
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
const { width, height } = this.getBoundingClientRect();
|
||||
try {
|
||||
studio.startViewer(
|
||||
this.objectId,
|
||||
this.skyboxId,
|
||||
"#studio_canvas",
|
||||
`#${this._canvasProvider!.canvasId}`,
|
||||
"Storyteller Studio",
|
||||
new ViewportSize(width, height),
|
||||
);
|
||||
@@ -47,10 +83,6 @@ export class ViewerElement extends LitElement {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<canvas id="studio_canvas" alt="Storyteller Studio"></canvas>
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
Reference in New Issue
Block a user