diff --git a/studio-bvh/src/joint.rs b/studio-bvh/src/joint.rs index 4632498..a1b0cf4 100644 --- a/studio-bvh/src/joint.rs +++ b/studio-bvh/src/joint.rs @@ -494,9 +494,7 @@ impl<'a> Iterator for Joints<'a> { }) } JointIterType::Sequence(ref mut joints) => { - let Some(next) = joints.pop_front() else { - return None; - }; + let next = joints.pop_front()?; if next >= self.joints.len() { return None; diff --git a/studio-frontend/src/app/api/EnqueueEngineCompositing.ts b/studio-frontend/src/app/api/EnqueueEngineCompositing.ts new file mode 100644 index 0000000..1865982 --- /dev/null +++ b/studio-frontend/src/app/api/EnqueueEngineCompositing.ts @@ -0,0 +1,19 @@ +import MakeRequest from "./MakeRequest"; + +export interface EnqueueEngineCompositingRequest { + uuid_idempotency_token: string, + media_file_token: string, + camera?: string, + camera_speed?: number, + skybox?: string, +} + +export interface EnqueueEngineCompositingResponse { + success: boolean, + inference_job_token?: string +} + +export const EnqueueEngineCompositing = MakeRequest({ + method: "POST", + routingFunction: () => `/v1/conversion/enqueue_render_engine_scene`, +}); \ No newline at end of file diff --git a/studio-frontend/src/app/api/MakeMultipartRequest.ts b/studio-frontend/src/app/api/MakeMultipartRequest.ts new file mode 100644 index 0000000..a9fa6d7 --- /dev/null +++ b/studio-frontend/src/app/api/MakeMultipartRequest.ts @@ -0,0 +1,18 @@ + +const MakeMultipartRequest = (endpoint = "", body: any) => { + const formData = new FormData(); + + Object.keys(body).forEach((key) => formData.append(key, body[key])); + + return fetch("https://funnel.tailce84f.ts.net/preview/", { + method: 'POST', + credentials: 'include', + headers: { + 'Accept': 'application/json', + }, + body: formData, + }) + .then(res => res.blob()); +} + +export default MakeMultipartRequest; diff --git a/studio-frontend/src/app/api/MakeRequest.ts b/studio-frontend/src/app/api/MakeRequest.ts new file mode 100644 index 0000000..142d9fc --- /dev/null +++ b/studio-frontend/src/app/api/MakeRequest.ts @@ -0,0 +1,79 @@ +type UrlRoutingFunction = (urlRouteArgs: UrlRouteArgs) => string; + +interface RequestHeaders { + [name: string]: string; +} + +interface RouteSetup { + method: string; + multipart?: boolean; + routingFunction: UrlRoutingFunction; +} + +const METHOD_OMITS_BODY: { [key: string]: boolean } = { + DELETE: false, + GET: true, + OPTIONS: true, + PATCH: false, + POST: false, + PUT: false, +}; + +export default < + UrlRouteArgs, + Request, + Response, + UrlParams extends Record, +>(routeSetup: RouteSetup) => { + return async function ( + urlRouteArgs: UrlRouteArgs, + request: Request, + queries?: UrlParams, + overrideHost?: string + ): Promise { + const newQueries = queries + ? Object + .keys(queries) + .map((key, i) => + `${ i ? "&" : "" }${ key }=${ + Array.isArray(queries[key]) + ? (queries[key] as unknown[]).join(`&${ key }=`) + : queries[key] + }` + ) + .join("") + : null; + + const endpoint = `${ + routeSetup.routingFunction(urlRouteArgs) + }${ + newQueries ? "?" + newQueries : "" + }`; + const method = routeSetup.method; + const methodOmitsBody = METHOD_OMITS_BODY[method] || false; + + console.log("🛼",request) + + return fetch( + `${ overrideHost ? overrideHost : "https://api.fakeyou.com" }${ endpoint }`, + { + method, + headers: { + "Accept": "application/json", + ...methodOmitsBody? {} : { "Content-Type": "application/json" } + }, + credentials: "include", + ...methodOmitsBody ? {} : { body: JSON.stringify(request) }, + }) + .then(res => res.json()) + .then(res => { + if (!res.success && res && (res.voice_token || res.dataset_token)) { + return res; + } + else if (res && "success" in res) { + return res; + } else Promise.reject(); + }) + .catch(e => ({ success : false })); + } +}; diff --git a/studio-frontend/src/app/api/VideoStylePreview.ts b/studio-frontend/src/app/api/VideoStylePreview.ts new file mode 100644 index 0000000..2b2482f --- /dev/null +++ b/studio-frontend/src/app/api/VideoStylePreview.ts @@ -0,0 +1,10 @@ +import MakeMultipartRequest from "./MakeMultipartRequest"; + +export interface VideoStylePreviewRequest { + video: File, + request: string +} + +export const VideoStylePreview = (request: VideoStylePreviewRequest) => { + return MakeMultipartRequest("",request); +} diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index ad35261..011106f 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -37,6 +37,7 @@ import "@storyteller/studio-web/studio"; import "@storyteller/studio-web/transform-toolbar"; import "./anim-editor.element"; import "./help.element"; +import "./temp-video-preview.element"; import "./app.element.scss"; @@ -59,6 +60,7 @@ export class AppElement extends LitElement { private api = new StorytellerApi(); #addToSceneButtonRef = createRef(); + #videoStylePreviewBtnRef = createRef(); #mediaLibraryRef = createRef(); #inspectorRef = createRef(); #hierarchyRef = createRef(); @@ -132,7 +134,7 @@ export class AppElement extends LitElement { - + ${this.#params.mode === StudioMode.Editor ? html` + + @@ -205,7 +213,7 @@ export class AppElement extends LitElement { ${this.renderSidebar()} @@ -223,7 +231,7 @@ export class AppElement extends LitElement {
- + ` : nothing} - + + + + + ${this.showHelp ? html`
` : nothing} - + ${this.loading ? html` diff --git a/studio-frontend/src/app/bubble-select.element.scss b/studio-frontend/src/app/bubble-select.element.scss new file mode 100644 index 0000000..31e7f57 --- /dev/null +++ b/studio-frontend/src/app/bubble-select.element.scss @@ -0,0 +1,57 @@ +@use "sass:math"; + +:host { + // background: red; + display: flex; + gap: 0.6rem; + box-sizing: border-box; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +.bubble { + position: relative; + flex: 0 0 auto; + display: flex; + align-items: flex-end; + width: 80px; + height: 80px; + padding: 0.5rem; + border-radius: 0.5rem; + background: { + size: cover; + position: center center; + } + + outline: #FFF2 solid 2px; + color: rgba(#fff, 0.75); + transition: { + property: background-color, outline-color; + duration: 200ms; + timing-function: ease; + } + white-space: nowrap; + + cursor: pointer; + + &.selected { + outline-color: var(--color-accent); + } + + .gradient { + position: absolute; + inset: 0; + background: linear-gradient(to top, #000B, #0000); + border-radius: 0.5rem; + } + + .label { + position: relative; + font: { + size: math.div(12, 16) * 1rem; + weight: 500; + } + } +} diff --git a/studio-frontend/src/app/bubble-select.element.ts b/studio-frontend/src/app/bubble-select.element.ts new file mode 100644 index 0000000..b08ca16 --- /dev/null +++ b/studio-frontend/src/app/bubble-select.element.ts @@ -0,0 +1,54 @@ +import { FormFieldBaseElement } from "@storyteller/studio-ui/forms/form-field-base"; +import { PropertyValues, html, unsafeCSS } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; +import { styleMap } from "lit/directives/style-map.js"; + +import { type StyleOption, type VstStyle } from "./inspector-tabs/style-tab"; + +import styles from "./bubble-select.element.scss?inline"; + +@customElement("sts-bubble-select") +export class BubbleSelect extends FormFieldBaseElement { + static override styles = unsafeCSS(styles); + + @property({ reflect: true }) + override role = "listbox" + + @property({ attribute: false }) + options: readonly StyleOption[] = []; + + @property() value?: VstStyle; + + protected override update(changes: PropertyValues): void { + if (changes.has("value")) + this.internals.setFormValue(this.value ?? ""); + + super.update(changes); + } + + protected override render = () => html` + ${ this.options.map(({ label, value, imageUrl }) => html` +
{ + // this.onChange({ target: { name: this.name, value: itemValue } }); + this.dispatchEvent(new CustomEvent("value-change", { + detail: value, + })) + } } + > +
+ + ${ label } + +
` + )} + ` +} diff --git a/studio-frontend/src/app/inspector-tabs/style-tab.scss b/studio-frontend/src/app/inspector-tabs/style-tab.scss new file mode 100644 index 0000000..bb22278 --- /dev/null +++ b/studio-frontend/src/app/inspector-tabs/style-tab.scss @@ -0,0 +1,5 @@ +.inspector-input-set { + label { + display: block; + } +} \ No newline at end of file diff --git a/studio-frontend/src/app/inspector-tabs/style-tab.ts b/studio-frontend/src/app/inspector-tabs/style-tab.ts new file mode 100644 index 0000000..a8056f1 --- /dev/null +++ b/studio-frontend/src/app/inspector-tabs/style-tab.ts @@ -0,0 +1,187 @@ +import { bind, on } from "@storyteller/framework"; +import { LitElement, TemplateResult, html, unsafeCSS } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { v4 as uuid } from "uuid"; + +import { EnqueueEngineCompositing } from "../api/EnqueueEngineCompositing"; + +import styles from "./style-tab.scss?inline"; + +export type VstStyle + = "anime_2d_flat" + | "cartoon_3d" + | "anime_2d" + | "anime_ghibli" + | "anime_retro_neon" + | "anime_standard" + | "comic_book" + | "ink_punk" + | "pixel_art" + | "ink_splash" + | "jojo_style" + | "pop_art" + | "realistic_1" + | "realistic_2" + | "ink_bw_style" + | "paper_origami" + ; + +export interface StyleOption { + label: string; + imageUrl: string; + value: VstStyle; +} + +export const STYLE_OPTIONS: readonly StyleOption[] = [ + { + label: "2D Anime", + imageUrl: "", + value: "anime_2d", + }, + { + label: "2D Anime (Flat)", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-2d-anime.webp", + value: "anime_2d_flat", + }, + { + label: "Anime Ghibli", + imageUrl: "", + value: "anime_ghibli", + }, + { + label: "Anime Retro Neon", + imageUrl: "", + value: "anime_retro_neon", + }, + { + label: "Anime Standard", + imageUrl: "", + value: "anime_standard", + }, + { + label: "3D Cartoon", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-3d-cartoon.webp", + value: "cartoon_3d", + }, + { + label: "Comic Book", + imageUrl: "", + value: "comic_book", + }, + { + label: "Ink Punk", + imageUrl: "", + value: "ink_punk", + }, + { + label: "Ink Splash", + imageUrl: "", + value: "ink_splash", + }, + { + label: "Ink B&W", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-ink-bw.webp", + value: "ink_bw_style", + }, + { + label: "Jojo Style", + imageUrl: "", + value: "jojo_style", + }, + { + label: "Origami", + imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-origami.webp", + value: "paper_origami", + }, + { + label: "Pixel Art", + imageUrl: "", + value: "pixel_art", + }, + { + label: "Pop Art", + imageUrl: "", + value: "pop_art", + }, + { + label: "Realistic 1", + imageUrl: "", + value: "realistic_1", + }, + { + label: "Realistic 2", + imageUrl: "", + value: "realistic_2", + }, +]; + +@customElement("sts-style-tab") +export class StyleTab extends LitElement { + static override styles = unsafeCSS(styles); + @state() vstStyle: VstStyle = "anime_2d_flat"; + @state() posPrompt = ""; + @state() negPrompt = ""; + @property() sceneToken = ""; + + inputChange = ({ target }: { target: { name: string, value: any } }) => { + // console.log("💁🏼‍♀️", this[target.name]); + // this[target.name] = target.value; // fix later + } + + @on("scene-state") + oAThing(): void { console.log("🍕")}; + + compositorEnqueue = (sceneToken: string) => () => { + EnqueueEngineCompositing("",{ + uuid_idempotency_token: uuid(), + media_file_token: sceneToken, + camera: "zoom", + camera_speed: 0.2, + skybox: "meadow_4k", + }).then((res: any) => { + if (res && res.success) { + window.parent.postMessage( + `studio-scene-composited:compositeJobToken:${ res.inference_job_token }`, + "*" + ); + // compositeJobTokenSet(res.inference_job_token) + } + }); + } + + thingy = ({ target }: any ) => { + } + + protected override render = (): TemplateResult => html` +
+ ${ this.vstStyle } + + + + + + + + + + + Generate your movie + +
+ `; +} diff --git a/studio-frontend/src/app/inspector.ts b/studio-frontend/src/app/inspector.ts new file mode 100644 index 0000000..2eb279e --- /dev/null +++ b/studio-frontend/src/app/inspector.ts @@ -0,0 +1,60 @@ +import { LitElement, type PropertyValues, html, nothing, unsafeCSS } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import "@storyteller/studio-ui/tree"; +import "./tabs.element"; +import "./bubble-select.element"; +import "./inspector-tabs/style-tab"; +import styles from "./inspector.element.scss?inline"; +import "@storyteller/studio-ui/button"; + +// const abc = "word"; + +enum InspectorMode { + Properties, + Style +} + +@customElement("sts-inspector") +export class Inspector extends LitElement { + static override styles = unsafeCSS(styles); + @state() selectedTab = InspectorMode.Style; + @property() sceneToken = ""; + @property() styleMediaToken = ""; + + tabs = [{ + label: "Properties", + value: InspectorMode.Properties + },{ + label: "Style", + value: InspectorMode.Style + }]; + + thingy = ({ target }: any) => { + console.log("🏎️", this.styleMediaToken); + } + + propertiesTab = html`
Properties
`; + + tabContent = () => { + switch (this.selectedTab) { + case InspectorMode.Properties: return this.propertiesTab; + case InspectorMode.Style: return html``; + } + } + + protected override render = () => html` +
+ + { + this.selectedTab = target.value; + } } + .tabs=${this.tabs} + .value=${this.selectedTab} + > +
+ ${ this.tabContent() } +
+
+ ` +} diff --git a/studio-frontend/src/app/style-select.element.scss b/studio-frontend/src/app/style-select.element.scss new file mode 100644 index 0000000..ecc942b --- /dev/null +++ b/studio-frontend/src/app/style-select.element.scss @@ -0,0 +1,53 @@ +@use "sass:math"; + +:host { + // background: red; + display: flex; + gap: 0.6rem; + box-sizing: border-box; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +.bubble { + position: relative; + flex: 0 0 auto; + display: flex; + align-items: flex-end; + width: 80px; + height: 80px; + padding: 0.5rem; + border-radius: 0.5rem; + background: { + size: cover; + position: center center; + } + + outline: #FFF2 solid 2px; + color: rgba(#fff, 0.75); + transition: { + property: background-color, outline-color; + duration: 200ms; + timing-function: ease; + } + white-space: nowrap; + + cursor: pointer; + + .gradient { + position: absolute; + inset: 0; + background: linear-gradient(to top, #000B, #0000); + border-radius: 0.5rem; + } + + .label { + position: relative; + font: { + size: math.div(12, 16) * 1rem; + weight: 500; + } + } +} diff --git a/studio-frontend/src/app/style-select.element.ts b/studio-frontend/src/app/style-select.element.ts new file mode 100644 index 0000000..14f0252 --- /dev/null +++ b/studio-frontend/src/app/style-select.element.ts @@ -0,0 +1,66 @@ +import { FormFieldBaseElement } from "@storyteller/studio-ui/forms/form-field-base"; +import { PropertyValues, html, unsafeCSS } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; +import { styleMap } from "lit/directives/style-map.js"; + +import { type StyleOption, type VstStyle } from "./inspector-tabs/style-tab"; + +import styles from "./style-select.element.scss?inline"; + +@customElement("sts-style-select") +export class StyleSelect extends FormFieldBaseElement { + static override styles = unsafeCSS(styles); + + @property({ reflect: true }) + override role = "listbox" + + @property({ attribute: false }) + options: readonly StyleOption[] = []; + + @property() value?: VstStyle; + + protected override update(changes: PropertyValues): void { + if (changes.has("value")) + this.internals.setFormValue(this.value ?? ""); + + super.update(changes); + } + + protected override render = () => html` +
+ + +
+ ` +} diff --git a/studio-frontend/src/app/tabs.element.scss b/studio-frontend/src/app/tabs.element.scss new file mode 100644 index 0000000..2093c3c --- /dev/null +++ b/studio-frontend/src/app/tabs.element.scss @@ -0,0 +1,24 @@ +.studio-tabs { + // background: red; + display: flex; + list-style: none; + margin-block-start: 0px; + margin-block-end: 0px; + padding-inline-start: 0px; + + li { + border: none; + padding: 1rem 1.4rem; + border-bottom: 4px solid transparent; + color: rgba(255, 255, 255, 0.5); + font-weight: 500; + white-space: nowrap; + + &.selected-tab { + border-bottom: 4px solid #e66462; + } + } + + + +} \ No newline at end of file diff --git a/studio-frontend/src/app/tabs.element.ts b/studio-frontend/src/app/tabs.element.ts new file mode 100644 index 0000000..5b644f7 --- /dev/null +++ b/studio-frontend/src/app/tabs.element.ts @@ -0,0 +1,26 @@ +import { LitElement, type PropertyValues, html, nothing, unsafeCSS } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +import styles from "./tabs.element.scss?inline"; + +@customElement("sts-tabs") +export class Tabs extends LitElement { + static override styles = unsafeCSS(styles); + @property() name = ""; + @property() onChange = (e: any) => {}; + @property() tabs = []; + @property() value = ""; + + protected override render = () => html` +
    + ${ this.tabs.map(({ label, value: itemValue },i) => + html`
  • { + this.onChange({ target: { name: this.name, value: itemValue } }); + } } + >${ label }
  • ` + )} +
+ ` +} \ No newline at end of file diff --git a/studio-frontend/src/app/temp-video-preview.element.scss b/studio-frontend/src/app/temp-video-preview.element.scss new file mode 100644 index 0000000..a432086 --- /dev/null +++ b/studio-frontend/src/app/temp-video-preview.element.scss @@ -0,0 +1,109 @@ +@use "sass:math"; + +.video-style-preview { + width: calc(100vw - 3rem); + height: calc(100vh - 3rem); + + &::part(content) { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2rem; + } + + &__side-by-side { + display: flex; + width: 100%; + } + + &__panel { + flex: 0 0 50%; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + } + + &__heading { + margin: 0; + font: { + family: "Fira Sans"; + size: 1rem; + weight: 500; + } + } + + &__image { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 0; + padding-top: calc((536 / 960) * 100%); + background: { + size: cover; + position: center center; + } + + * { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + } + } + + &__prompts { + display: flex; + gap: 1rem; + } + + &__form-field { + display: flex; + flex-direction: column; + gap: 0.75rem; + + &--base-style { + align-items: center; + } + } + + &__label { + font: { + family: "Fira Sans"; + size: math.div(15, 16) * 1rem; + weight: 500; + } + } + + &__input { + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + gap: 0.75rem; + + background: #0004; + border: 2px solid #FFF2; + color: #FFFC; + transition: { + property: background-color, border-color, color; + duration: 33.333ms; + timing-function: linear; + } + + font: { + family: "Fira Sans", sans-serif; + size: math.div(15, 16) * 1rem; + } + + &:focus { + outline: none; + border-color: var(--color-accent); + } + } +} diff --git a/studio-frontend/src/app/temp-video-preview.element.ts b/studio-frontend/src/app/temp-video-preview.element.ts new file mode 100644 index 0000000..e914d7d --- /dev/null +++ b/studio-frontend/src/app/temp-video-preview.element.ts @@ -0,0 +1,271 @@ +import * as studio from "@storyteller/studio"; +import { LitElement, type PropertyValues, html, nothing, unsafeCSS, TemplateResult } from "lit"; +import { type Ref, createRef, ref } from "lit/directives/ref.js"; +import { customElement, property, state } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { styleMap } from "lit/directives/style-map.js"; +import { type ScreenshotReadyEvent } from "@storyteller/studio"; +import { DialogElement } from "@storyteller/studio-ui/dialog"; +import { bind, on } from "@storyteller/framework"; + +import { VideoStylePreview } from "./api/VideoStylePreview"; + +import "./style-select.element"; + +import styles from "./temp-video-preview.element.scss?inline"; +import { STYLE_OPTIONS, type VstStyle } from "./inspector-tabs/style-tab"; + +enum InspectorMode { + Properties, + Style +} + +@customElement("sts-temp-video-preview") +export class TempVideoPreview extends LitElement { + @property({ attribute: false }) + trigger?: Ref; + + @state() sourceBlob?: Blob; + @state() resultBlob?: Blob; + + @state() sourceImageUrl?: string; + @state() resultImageUrl?: string; + + @state() selectedStyle: VstStyle = "anime_2d_flat"; + @state() prompt = ""; + @state() negPrompt = ""; + + #temporaryVideoPreviewRef = createRef(); + + protected override willUpdate(changes: PropertyValues): void { + if (changes.has("trigger")) { + const prevTrigger = changes.get("trigger"); + prevTrigger?.value?.removeEventListener("click", this.onDialogOpen); + + this.trigger?.value?.addEventListener("click", this.onDialogOpen); + } + + super.willUpdate(changes) + } + + override disconnectedCallback(): void { + this.trigger?.value?.removeEventListener("click", this.onDialogOpen); + + if (this.sourceImageUrl) { + URL.revokeObjectURL(this.sourceImageUrl); + this.sourceImageUrl = undefined; + } + + if (this.resultImageUrl) { + URL.revokeObjectURL(this.resultImageUrl); + this.resultImageUrl = undefined; + } + + super.disconnectedCallback(); + } + + requestScreen = () => { + studio.requestScreenshot(); + console.log("🟢"); + } + + onDialogOpen = () => { + studio.requestScreenshot(); + } + + @on("window:screenshot-ready") + async onScreenshotReady (event: ScreenshotReadyEvent) { + console.log("🟣",event.detail.data); + const file = new File( + [event.detail.data], + "studio-scene-screen.png", + { type: "image/png" }, + ); + this.sourceBlob = file; + + const prevSourceUrl = this.sourceImageUrl; + this.sourceImageUrl = URL.createObjectURL(this.sourceBlob); + + if (prevSourceUrl) + URL.revokeObjectURL(prevSourceUrl); + + this.resultImageUrl = undefined; + + try { + this.resultBlob = await VideoStylePreview({ + video: file, + request: + JSON.stringify({ + "style": this.selectedStyle, + "positive_prompt": this.prompt, + "negative_prompt": this.negPrompt, + }) + }); + console.log("🟡", this.resultBlob); + } + catch (err) { + // TODO: Show error message + console.error(err); + return; + } + + const prevResultUrl = this.resultImageUrl; + this.resultImageUrl = URL.createObjectURL(this.resultBlob); + + if (prevResultUrl) + URL.revokeObjectURL(prevResultUrl); + } + + 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)$/.test(event.key)) + return; + + event.stopPropagation(); + } + + protected override render = (): TemplateResult => html` + { + this.#temporaryVideoPreviewRef.value?.close(); + }} + @keydown=${this.stopTextInputBubbling} + @keyup=${this.stopTextInputBubbling} + > + + Video Style Preview + + + + +
+
+

+ Raw Preview +

+
+ ${!this.sourceImageUrl ? html` +

+ Loading... +

+ ` : nothing} +
+
+
+

+ Styled Preview +

+
+ ${!this.resultImageUrl ? html` +

+ Loading... +

+ ` : nothing} +
+
+
+ +
+ + + + +
+ +
+
+ + +
+
+ + +
+
+ + + + Update + + + + `] as const} + >
+ ` +} diff --git a/studio-ui/src/lib/button.element.scss b/studio-ui/src/lib/button.element.scss index 42c2d83..8ec7210 100644 --- a/studio-ui/src/lib/button.element.scss +++ b/studio-ui/src/lib/button.element.scss @@ -16,7 +16,7 @@ cursor: pointer; user-select: none; - background: #0006; + background: var(--background-color, #0006); color: #FFFC; backdrop-filter: blur(16px); transition: { diff --git a/studio-ui/src/lib/button.element.ts b/studio-ui/src/lib/button.element.ts index 2a532e0..06d25bc 100644 --- a/studio-ui/src/lib/button.element.ts +++ b/studio-ui/src/lib/button.element.ts @@ -16,16 +16,27 @@ export class ButtonElement extends LitElement { @property() icon?: string; @property({ type: Boolean }) disabled = false; + @property({ type: Boolean }) + primary?: boolean; + override tabIndex = 0; - protected override willUpdate(changes: PropertyValues): void { + protected override update(changes: PropertyValues): void { if (changes.has("disabled")) { this.ariaDisabled = this.disabled ? "true" : null; this.tabIndex = this.disabled ? -1 : 0; this.classList.toggle("disabled", this.disabled); } - super.willUpdate(changes); + if (changes.has("primary")) { + if (this.primary) { + this.style.setProperty("--background-color", "var(--color-accent)"); + } else { + this.style.removeProperty("--background-color"); + } + } + + super.update(changes); } @on("keydown") diff --git a/studio-ui/src/lib/dialog/dialog.element.ts b/studio-ui/src/lib/dialog/dialog.element.ts index 9800fcc..c5c1e72 100644 --- a/studio-ui/src/lib/dialog/dialog.element.ts +++ b/studio-ui/src/lib/dialog/dialog.element.ts @@ -392,6 +392,11 @@ export class DialogElement extends LitElement { trigger.addEventListener("click", this.onTriggerClick); } + if (changes.has("template")) { + if (this.#overlayHandle && this.template) + this._overlay.reRender(this.#overlayHandle, this.template); + } + super.updated(changes); } diff --git a/studio-ui/src/lib/forms/index.ts b/studio-ui/src/lib/forms/index.ts index ae82dc4..9f1ad51 100644 --- a/studio-ui/src/lib/forms/index.ts +++ b/studio-ui/src/lib/forms/index.ts @@ -1,2 +1,3 @@ +export * from "./form-field-base.element"; export * from "./text-field.element"; export * from "./scalar-field.element"; diff --git a/studio-ui/src/lib/icon.element.ts b/studio-ui/src/lib/icon.element.ts index a5610e9..952733d 100644 --- a/studio-ui/src/lib/icon.element.ts +++ b/studio-ui/src/lib/icon.element.ts @@ -45,6 +45,7 @@ import farMagnifyingGlass from "@fortawesome/fontawesome-pro/svgs/regular/magnif import fasHouse from "@fortawesome/fontawesome-pro/svgs/solid/house.svg?raw"; import fasRecord from "@fortawesome/fontawesome-pro/svgs/solid/circle.svg?raw"; import fasUpload from "@fortawesome/fontawesome-pro/svgs/solid/upload.svg?raw"; +import fasFilms from "@fortawesome/fontawesome-pro/svgs/solid/films.svg?raw"; import customCylinder from "../assets/icons/cylinder.svg?raw"; import customSphere from "../assets/icons/sphere.svg?raw"; @@ -91,6 +92,7 @@ function renderSvg(iconId: string) { "figure": () => fasPersonRunning, "file-import": () => farFileImport, "film": () => farFilm, + "films": () => fasFilms, "floppy-disk": () => fasFloppyDisk, "fwd-fast": () => fasFwdFast, "fwd-step": () => fasFwdStep, diff --git a/studio-ui/src/lib/menu.element.ts b/studio-ui/src/lib/menu.element.ts index 6c51893..f1d9a99 100644 --- a/studio-ui/src/lib/menu.element.ts +++ b/studio-ui/src/lib/menu.element.ts @@ -1,5 +1,5 @@ import { inject, on } from "@storyteller/framework"; -import { LitElement, PropertyValues, css, html, unsafeCSS } from "lit"; +import { LitElement, PropertyValues, TemplateResult, css, html, unsafeCSS } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { createRef, ref, type Ref } from "lit/directives/ref.js"; @@ -82,6 +82,8 @@ export class MenuElement extends LitElement { #panelRef = createRef(); #handle?: WeakRef; + #template?: TemplateResult; + show(): void { const trigger = this.trigger.value; if (!trigger) return; @@ -90,8 +92,7 @@ export class MenuElement extends LitElement { trigger.ariaExpanded = "true"; const rect = trigger.getBoundingClientRect(); - - this.#handle = this._overlay.attach(this, html` + this.#template = html` 's slot this.#slotRef.value?.assignedNodes() } - `); + `; + + this.#handle = this._overlay.attach(this, this.#template); } hide(): void { @@ -134,8 +137,9 @@ export class MenuElement extends LitElement { protected override update(changes: PropertyValues): void { super.update(changes); - if (this.#handle) - this._overlay.reRender(this.#handle); + + if (this.#handle && this.#template) + this._overlay.reRender(this.#handle, [this, this.#template]); } protected override updated(changes: PropertyValues): void { diff --git a/studio-ui/src/lib/overlay-provider.element.ts b/studio-ui/src/lib/overlay-provider.element.ts index 9a4a66e..40b9605 100644 --- a/studio-ui/src/lib/overlay-provider.element.ts +++ b/studio-ui/src/lib/overlay-provider.element.ts @@ -83,7 +83,7 @@ export interface OverlayProvider { detach(handle: WeakRef): void; /** Re-render a currently-attached overlay. */ - reRender(handle: WeakRef): void; + reRender(handle: WeakRef, template: [LitElement, TemplateResult]): void; } export interface AttachOptions { @@ -226,19 +226,14 @@ export class OverlayProviderElement // TODO: I'm not actually sure whether this serves a necessary function. // Needs further testing. - reRender(handle: WeakRef): void { + reRender(handle: WeakRef, template: [LitElement, TemplateResult]): void { const index = handle.deref()?.id; if (index == null || index === -1) return; - const container = this.#containerRefs[index]?.value; - if (!container) return; + this.#templates[index] = template; - const tuple = this.#templates[index]; - if (!tuple) return; - - const [host, template] = tuple; - render(template, container, { host }); + this.#updateLightDom(); } #updateLightDom(): void { diff --git a/studio-ui/src/lib/tabs.element.ts b/studio-ui/src/lib/tabs.element.ts new file mode 100644 index 0000000..d74c486 --- /dev/null +++ b/studio-ui/src/lib/tabs.element.ts @@ -0,0 +1,17 @@ +import { LitElement, type PropertyValues, html, nothing, unsafeCSS } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +// import styles from "./button.element.scss?inline"; + +// const abc = "word"; + +@customElement("sts-tabs") +export class Tabs extends LitElement { + @property() tabs = ["hello world"]; + + protected override render = () => html` +
+ ${ this.tabs.map((item,i) => item) } +
+ ` +} \ No newline at end of file diff --git a/test_headless_mode.sh b/test_headless_mode.sh new file mode 100755 index 0000000..43d61ee --- /dev/null +++ b/test_headless_mode.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Update this to a recent scene to download it. +scene_url="https://storage.googleapis.com/vocodes-public/media/d/2/d/s/e/d2dseec97gw06fk7mdavmk4pv87dk0dt/scene_d2dseec97gw06fk7mdavmk4pv87dk0dt.scn.ron" + +filename="downloaded_scene.scn.ron" +directory="studio/assets/" +full_path="${directory}${filename}" + +if [ ! -f "${full_path}" ]; then + echo "Downloading ${scene_url}" + mkdir -p "${directory}" + wget "${scene_url}" -O "${full_path}" +fi + +cargo run -p studio --bin studio-headless \ + -- \ + scene \ + -o frames/ \ + --frames 100 \ + --skybox "00ff00" \ + $filename + diff --git a/tsconfig.base.json b/tsconfig.base.json index 7b67ed2..df667bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -26,6 +26,7 @@ "@storyteller/studio-ui/progress": ["studio-ui/src/lib/progress.element.ts"], "@storyteller/studio-ui/toolbar": ["studio-ui/src/lib/toolbar.element.ts"], "@storyteller/studio-ui/tree": ["studio-ui/src/lib/tree.element.ts"], + "@storyteller/studio-ui/forms/form-field-base": ["studio-ui/src/lib/forms/form-field-base.element.ts"], "@storyteller/studio-ui/forms/text-field": ["studio-ui/src/lib/forms/text-field.element.ts"], "@storyteller/studio-ui/forms/scalar-field": ["studio-ui/src/lib/forms/scalar-field.element.ts"], "@storyteller/studio-web": ["studio-web/src/index.ts"],