From 07456eabc7e94f7b5676947505715001d717927f Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Tue, 5 Dec 2023 17:25:06 -0500 Subject: [PATCH] `CanvasProvider` to juggle a single Bevy canvas --- studio-web/package.json | 1 + .../src/lib/canvas-provider.element.scss | 3 + studio-web/src/lib/canvas-provider.element.ts | 94 +++++++++++++++++++ studio-web/src/lib/viewer.element.scss | 13 +++ studio-web/src/lib/viewer.element.ts | 49 ++++++++-- tsconfig.base.json | 1 + 6 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 studio-web/src/lib/canvas-provider.element.scss create mode 100644 studio-web/src/lib/canvas-provider.element.ts diff --git a/studio-web/package.json b/studio-web/package.json index 1602a83..c9dd07d 100644 --- a/studio-web/package.json +++ b/studio-web/package.json @@ -6,6 +6,7 @@ "module": "./index.mjs", "typings": "./index.d.ts", "exports": { + "./canvas-provider": "./canvas-provider.element.js", "./viewer": "./viewer.element.js" } } diff --git a/studio-web/src/lib/canvas-provider.element.scss b/studio-web/src/lib/canvas-provider.element.scss new file mode 100644 index 0000000..7e928ca --- /dev/null +++ b/studio-web/src/lib/canvas-provider.element.scss @@ -0,0 +1,3 @@ +::slotted([slot="canvas"]) { + display: none; +} diff --git a/studio-web/src/lib/canvas-provider.element.ts b/studio-web/src/lib/canvas-provider.element.ts new file mode 100644 index 0000000..9a56fed --- /dev/null +++ b/studio-web/src/lib/canvas-provider.element.ts @@ -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 = 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` + + + `; + + #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; + } +} diff --git a/studio-web/src/lib/viewer.element.scss b/studio-web/src/lib/viewer.element.scss index e69de29..e9de20f 100644 --- a/studio-web/src/lib/viewer.element.scss +++ b/studio-web/src/lib/viewer.element.scss @@ -0,0 +1,13 @@ +:host { + display: block; + position: relative; + width: 100%; + height: 100%; +} + +::slotted([slot="canvas"]) { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} diff --git a/studio-web/src/lib/viewer.element.ts b/studio-web/src/lib/viewer.element.ts index e83ea3b..5b3652c 100644 --- a/studio-web/src/lib/viewer.element.ts +++ b/studio-web/src/lib/viewer.element.ts @@ -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,29 @@ 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.#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): void { @@ -28,17 +52,26 @@ export class ViewerElement extends LitElement { } } + protected override render = () => html` + + `; + + #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 +80,6 @@ export class ViewerElement extends LitElement { throw err; } } - - protected override render = () => html` - - `; } declare global { diff --git a/tsconfig.base.json b/tsconfig.base.json index ffb5726..f6337bf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,7 @@ "@storyteller/framework": ["framework/src/index.ts"], "@storyteller/studio": ["dist/studio"], "@storyteller/studio-web": ["studio-web/src/index.ts"], + "@storyteller/studio-web/canvas-provider": ["studio-web/src/lib/canvas-provider.element.ts"], "@storyteller/studio-web/viewer": ["studio-web/src/lib/viewer.element.ts"], "@storyteller/utility": ["utility/src/index.ts"] }