import { debounce, drag, on } from "@storyteller/framework"; import * as studio from "@storyteller/studio"; import { SceneState, type SceneStateEvent, type ScreenshotReadyEvent, StudioMode, } from "@storyteller/studio"; import { match } from "@storyteller/utility"; import { LitElement, type PropertyValues, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; import { classMap } from "lit/directives/class-map.js"; import "@storyteller/studio-web/canvas-provider"; import "@storyteller/studio-web/scene-hierarchy"; import "@storyteller/studio-web/studio"; import "@storyteller/studio-web/transform-toolbar"; import "./app.element.scss" @customElement("app-root") export class AppElement extends LitElement { @state() imageCaptureUrl?: string; @state() hierarchyWidth = 256; @state() resizing = false; @state() loading = true; @state() prompt = ""; @state() negPrompt = ""; @state() seed = randomSeed(); protected override createRenderRoot = () => this; override disconnectedCallback(): void { if (this.imageCaptureUrl != null) URL.revokeObjectURL(this.imageCaptureUrl); super.disconnectedCallback(); } protected override willUpdate(changes: PropertyValues): void { if ( changes.has("prompt") || changes.has("negPrompt") || changes.has("seed") || changes.has("imageCaptureUrl") ) { // TODO: Send parameters to model console.log("changes:", { prompt: this.prompt, negPrompt: this.negPrompt, seed: this.seed, imageCaptureUrl: this.imageCaptureUrl, }); } super.willUpdate(changes); } @on("scene-state") onSceneStateChange({ detail: state }: SceneStateEvent): void { console.log("scene state change:", SceneState[state]); this.loading = match (state, { [SceneState.Active]: () => false, _: () => true, }); } @on("entity-changes") @debounce(1000 / 24) onStudioChanges(): void { studio.requestScreenshot(); } @on("viewport-resized") // This handler requires a longer debounce to avoid crashing the render piptline @debounce(1000 / 5) onViewportResized(): void { studio.requestScreenshot(); } @on("screenshot-ready") onScreenshotReady({ detail: screenshot }: ScreenshotReadyEvent) { const prevUrl = this.imageCaptureUrl; const blob = new Blob([screenshot.data.buffer], { type: "image/jpg" }); this.imageCaptureUrl = URL.createObjectURL(blob); if (prevUrl != null) URL.revokeObjectURL(prevUrl); } updatePrompt(event: Event): void { this.prompt = (event.target as HTMLInputElement).value; } updateNegPrompt(event: Event): void { this.negPrompt = (event.target as HTMLInputElement).value; } refreshSeed(): void { this.seed = randomSeed(); } stopTextInputBubbling(event: KeyboardEvent): void { // We want modifier keys like "Alt" to always be received by the Bevy // canvas because they affect the camera controller behavior, but we do // NOT want Bevy to receive typing inputs like W, A, S, and D while the // user is trying to type into the Prompt field. if (/^(Alt|Control|Shift)$/.test(event.key)) return; event.stopPropagation(); } onResizeStart(): void { this.resizing = true; } onResizeEnd(): void { this.resizing = false; } onResize(event: PointerEvent): void { let updated = this.hierarchyWidth + event.movementX; this.hierarchyWidth = Math.max(Math.min(updated, 512), 128); } protected override render = () => html`
${this.imageCaptureUrl != null ? html` Image capture ` : html`
`}
{ if (event.key === "Enter") { event.preventDefault(); this.updatePrompt(event); } }} />
{ if (event.key === "Enter") { event.preventDefault(); this.updateNegPrompt(event); } }} />
${/* FIXME: Real-time capture is not really feasible until we can come up with a more elegant way to exclude the world grid, gizmos, etc. from the captured image
*/ nothing} ${this.loading ? html`

Loading

` : nothing}
` } function randomSeed(): number { return Math.round(Math.random() * 99999); }