CanvasProvider to juggle a single Bevy canvas

This commit is contained in:
Danny McGee
2023-12-05 17:25:06 -05:00
parent b2ed422d14
commit 07456eabc7
6 changed files with 151 additions and 10 deletions
+1
View File
@@ -6,6 +6,7 @@
"module": "./index.mjs",
"typings": "./index.d.ts",
"exports": {
"./canvas-provider": "./canvas-provider.element.js",
"./viewer": "./viewer.element.js"
}
}
@@ -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;
}
}
+13
View File
@@ -0,0 +1,13 @@
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
}
::slotted([slot="canvas"]) {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
+39 -10
View File
@@ -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<this>): void {
@@ -28,17 +52,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 +80,6 @@ export class ViewerElement extends LitElement {
throw err;
}
}
protected override render = () => html`
<canvas id="studio_canvas" alt="Storyteller Studio"></canvas>
`;
}
declare global {
+1
View File
@@ -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"]
}