mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-09 00:09:55 +00:00
Add WIP video style preview dialog (#115)
Co-authored-by: Victor <victor@storyteller.ai>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// import GetApiHost from "./GetApiHost";
|
||||
|
||||
// const { formatUrl, host = "" } = GetApiHost();
|
||||
|
||||
const MakeMultipartRequest = (endpoint = "", body: any) => {
|
||||
const formData = new FormData();
|
||||
|
||||
Object.keys(body).forEach((key) => formData.append(key, body[key]));
|
||||
|
||||
// formData.append('source', "video");
|
||||
|
||||
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;
|
||||
@@ -8,8 +8,8 @@ interface RouteSetup<UrlRouteArgs> {
|
||||
method: string;
|
||||
multipart?: boolean;
|
||||
routingFunction: UrlRoutingFunction<UrlRouteArgs>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const METHOD_OMITS_BODY: { [key: string]: boolean } = {
|
||||
DELETE: false,
|
||||
GET: true,
|
||||
@@ -19,25 +19,50 @@ const METHOD_OMITS_BODY: { [key: string]: boolean } = {
|
||||
PUT: false,
|
||||
};
|
||||
|
||||
const MakeRequest = <UrlRouteArgs, Request, Response, UrlParams>(routeSetup: RouteSetup<UrlRouteArgs>) : (urlRouteArgs: UrlRouteArgs, request: Request, queries?: UrlParams) => Promise<Response> => {
|
||||
return async function(urlRouteArgs: UrlRouteArgs, request: Request, queries?: any) : Promise<Response> {
|
||||
const newQueries = queries ? Object.keys(queries).map((key, i) => {
|
||||
return `${ i ? "&" : "" }${ key }=${ Array.isArray(queries[key]) ? queries[key].join(`&${ key }=`) : queries[key] }`
|
||||
}).join("") : null;
|
||||
export default <
|
||||
UrlRouteArgs,
|
||||
Request,
|
||||
Response,
|
||||
UrlParams extends Record<string, unknown>,
|
||||
>(routeSetup: RouteSetup<UrlRouteArgs>) => {
|
||||
return async function (
|
||||
urlRouteArgs: UrlRouteArgs,
|
||||
request: Request,
|
||||
queries?: UrlParams,
|
||||
overrideHost?: string
|
||||
): Promise<Response> {
|
||||
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 endpoint = `${
|
||||
routeSetup.routingFunction(urlRouteArgs)
|
||||
}${
|
||||
newQueries ? "?" + newQueries : ""
|
||||
}`;
|
||||
const method = routeSetup.method;
|
||||
const methodOmitsBody = METHOD_OMITS_BODY[method] || false;
|
||||
|
||||
console.log("🛼",request)
|
||||
|
||||
return fetch(`https://api.fakeyou.com${ endpoint }`, {
|
||||
method,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
...methodOmitsBody? {} : { "Content-Type": "application/json" }
|
||||
return fetch(
|
||||
`${ overrideHost ? overrideHost : "https://api.fakeyou.com" }${ endpoint }`,
|
||||
{
|
||||
method,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
...methodOmitsBody? {} : { "Content-Type": "application/json" }
|
||||
},
|
||||
credentials: 'include',
|
||||
credentials: "include",
|
||||
...methodOmitsBody ? {} : { body: JSON.stringify(request) },
|
||||
})
|
||||
.then(res => res.json())
|
||||
@@ -52,5 +77,3 @@ const MakeRequest = <UrlRouteArgs, Request, Response, UrlParams>(routeSetup: Rou
|
||||
.catch(e => ({ success : false }));
|
||||
}
|
||||
};
|
||||
|
||||
export default MakeRequest;
|
||||
@@ -0,0 +1,10 @@
|
||||
import MakeMultipartRequest from "./MakeMultipartRequest";
|
||||
|
||||
export interface VideoStylePreviewRequest {
|
||||
video: File,
|
||||
request: string
|
||||
}
|
||||
|
||||
export const VideoStylePreview = (request: VideoStylePreviewRequest) => {
|
||||
return MakeMultipartRequest("",request);
|
||||
}
|
||||
@@ -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";
|
||||
import { mock_save_load } from "./util";
|
||||
@@ -60,6 +61,7 @@ export class AppElement extends LitElement {
|
||||
private api = new StorytellerApi();
|
||||
|
||||
#addToSceneButtonRef = createRef<ButtonElement>();
|
||||
#videoStylePreviewBtnRef = createRef<ButtonElement>();
|
||||
#mediaLibraryRef = createRef<DialogElement>();
|
||||
#inspectorRef = createRef<HTMLDivElement>();
|
||||
#hierarchyRef = createRef<SceneHierarchyElement>();
|
||||
@@ -161,7 +163,7 @@ export class AppElement extends LitElement {
|
||||
<sts-scene-data-provider>
|
||||
<sts-overlay-provider>
|
||||
<sts-iframe-messaging-bridge></sts-iframe-messaging-bridge>
|
||||
|
||||
|
||||
${this.#params.mode === StudioMode.Editor ? html`
|
||||
<sts-add-to-scene-menu
|
||||
.trigger=${this.#addToSceneButtonRef}
|
||||
@@ -220,6 +222,12 @@ export class AppElement extends LitElement {
|
||||
class="add-to-scene-dropdown"
|
||||
>
|
||||
</sts-button>
|
||||
<sts-button
|
||||
${ref(this.#videoStylePreviewBtnRef)}
|
||||
icon="films"
|
||||
class="video-style-preview-dropdown"
|
||||
>
|
||||
</sts-button>
|
||||
<sts-transform-toolbar></sts-transform-toolbar>
|
||||
</div>
|
||||
|
||||
@@ -234,7 +242,7 @@ export class AppElement extends LitElement {
|
||||
<div class="sidebar-header">
|
||||
${this.sidebars.map(({title, name}) => {
|
||||
return html`<sts-button class=${classMap({"current-tab": name === this.currentSidebar, "sidebar-tab": true})}>${title}</sts-button>`
|
||||
})}
|
||||
})}
|
||||
</div>
|
||||
|
||||
${this.renderSidebar()}
|
||||
@@ -252,7 +260,7 @@ export class AppElement extends LitElement {
|
||||
<div class="entity-inspector">
|
||||
<sts-entity-inspector></sts-entity-inspector>
|
||||
</div>
|
||||
|
||||
|
||||
<sts-anim-editor
|
||||
class="timeline"
|
||||
style=${styleMap({
|
||||
@@ -260,14 +268,19 @@ export class AppElement extends LitElement {
|
||||
})}
|
||||
></sts-anim-editor>
|
||||
` : nothing}
|
||||
|
||||
|
||||
|
||||
<sts-temp-video-preview
|
||||
.trigger=${ this.#videoStylePreviewBtnRef }>
|
||||
</sts-temp-video-preview>
|
||||
|
||||
<!-- Dialogs and modals -->
|
||||
${this.showHelp ? html`
|
||||
<div class="help">
|
||||
<sts-help></sts-help>
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
${this.loading ? html`
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
.studio-bubble-select {
|
||||
@use "sass:math";
|
||||
|
||||
:host {
|
||||
// background: red;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin-block-start: 0px;
|
||||
margin-block-end: 0px;
|
||||
padding-inline-start: 0px;
|
||||
gap: 0.6rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
li {
|
||||
flex: 0 0 auto;
|
||||
border: 2px solid darken(purple, 3%);
|
||||
padding: 8px 16px;
|
||||
border-radius: 0.5rem;
|
||||
background-color: darken(purple, 3%);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: rgba(#fff, 0.75);
|
||||
white-space: nowrap;
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&.selected-bubble {
|
||||
color: #fff;
|
||||
border-color: red;
|
||||
background-color: lighten(purple, 6%);
|
||||
|
||||
&.fill {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,54 @@
|
||||
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
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";
|
||||
|
||||
// const abc = "word";
|
||||
|
||||
@customElement("sts-bubble-select")
|
||||
export class BubbleSelect extends LitElement {
|
||||
export class BubbleSelect extends FormFieldBaseElement {
|
||||
static override styles = unsafeCSS(styles);
|
||||
@property() name = "";
|
||||
@property() onChange = (e: any) => {};
|
||||
@property() options = [];
|
||||
@property() value = "";
|
||||
|
||||
@property({ reflect: true })
|
||||
override role = "listbox"
|
||||
|
||||
@property({ attribute: false })
|
||||
options: readonly StyleOption[] = [];
|
||||
|
||||
@property() value?: VstStyle;
|
||||
|
||||
protected override update(changes: PropertyValues<this>): void {
|
||||
if (changes.has("value"))
|
||||
this.internals.setFormValue(this.value ?? "");
|
||||
|
||||
super.update(changes);
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<ul class="studio-bubble-select">
|
||||
${ this.options.map(({ label, value: itemValue },i) =>
|
||||
html`<li
|
||||
class=${ itemValue === this.value ? "selected-bubble" : "" }
|
||||
@click=${ ({ target }: any) => {
|
||||
this.onChange({ target: { name: this.name, value: itemValue } });
|
||||
} }
|
||||
>${ label }</li>`
|
||||
)}
|
||||
</ul>
|
||||
${ this.options.map(({ label, value, imageUrl }) => html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"bubble": true,
|
||||
"selected": value === this.value,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"background-image": `url(${imageUrl})`,
|
||||
})}
|
||||
@click=${() => {
|
||||
// this.onChange({ target: { name: this.name, value: itemValue } });
|
||||
this.dispatchEvent(new CustomEvent("value-change", {
|
||||
detail: value,
|
||||
}))
|
||||
} }
|
||||
>
|
||||
<div class="gradient"></div>
|
||||
<span class="label">
|
||||
${ label }
|
||||
</span>
|
||||
</div>`
|
||||
)}
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,56 @@
|
||||
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
||||
import { bind, on } from "@storyteller/framework";
|
||||
import { LitElement, TemplateResult, html, unsafeCSS } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import styles from "./style-tab.scss?inline";
|
||||
import { EnqueueEngineCompositing } from "../api/EnqueueEngineCompositing";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { drag, on, provide } from "@storyteller/framework";
|
||||
|
||||
import { EnqueueEngineCompositing } from "../api/EnqueueEngineCompositing";
|
||||
|
||||
import styles from "./style-tab.scss?inline";
|
||||
|
||||
export type VstStyle
|
||||
= "anime_2d_flat"
|
||||
| "cartoon_3d"
|
||||
| "ink_bw_style"
|
||||
| "paper_origami"
|
||||
;
|
||||
|
||||
export interface StyleOption {
|
||||
label: string;
|
||||
imageUrl: string;
|
||||
value: VstStyle;
|
||||
}
|
||||
|
||||
export const STYLE_OPTIONS: readonly StyleOption[] = [
|
||||
{
|
||||
label: "2D Anime",
|
||||
imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-2d-anime.webp",
|
||||
value: "anime_2d_flat",
|
||||
},
|
||||
{
|
||||
label: "3D Cartoon",
|
||||
imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-3d-cartoon.webp",
|
||||
value: "cartoon_3d",
|
||||
},
|
||||
{
|
||||
label: "Ink B&W",
|
||||
imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-ink-bw.webp",
|
||||
value: "ink_bw_style",
|
||||
},
|
||||
{
|
||||
label: "Origami",
|
||||
imageUrl: "https://fakeyou.com/images/landing/onboarding/styles/style-origami.webp",
|
||||
value: "paper_origami",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("sts-style-tab")
|
||||
export class StyleTab extends LitElement {
|
||||
static override styles = unsafeCSS(styles);
|
||||
@state() vstStyle = "anime_2d_flat";
|
||||
@state() vstStyle: VstStyle = "anime_2d_flat";
|
||||
@state() posPrompt = "";
|
||||
@state() negPrompt = "";
|
||||
@property() sceneToken = "";
|
||||
|
||||
styleOptions = [
|
||||
{
|
||||
label: "2D Anime",
|
||||
imageUrl: "/images/landing/onboarding/styles/style-2d-anime.webp",
|
||||
value: "anime_2d_flat",
|
||||
},
|
||||
{
|
||||
label: "3D Cartoon",
|
||||
imageUrl: "/images/landing/onboarding/styles/style-3d-cartoon.webp",
|
||||
value: "cartoon_3d",
|
||||
},
|
||||
{
|
||||
label: "Ink B&W",
|
||||
imageUrl: "/images/landing/onboarding/styles/style-ink-bw.webp",
|
||||
value: "ink_bw_style",
|
||||
},
|
||||
{
|
||||
label: "Origami",
|
||||
imageUrl: "/images/landing/onboarding/styles/style-origami.webp",
|
||||
value: "paper_origami",
|
||||
},
|
||||
];
|
||||
|
||||
inputChange = ({ target }: { target: { name: string, value: any } }) => {
|
||||
// console.log("💁🏼♀️", this[target.name]);
|
||||
// this[target.name] = target.value; // fix later
|
||||
@@ -69,32 +84,36 @@ export class StyleTab extends LitElement {
|
||||
console.log("🏎️", this.sceneToken);
|
||||
}
|
||||
|
||||
protected override render = () => html`<section class="inspector-input-set">
|
||||
${ this.vstStyle }
|
||||
<button @click=${ this.thingy }>State</button>
|
||||
<sts-bubble-select
|
||||
.onChange=${ ({ target }: any) => { this.vstStyle = target.value; } }
|
||||
.options=${this.styleOptions}
|
||||
.value=${this.vstStyle}
|
||||
></sts-bubble-select>
|
||||
<label htmlFor="inspector-posPrompt">Positive Prompt</label>
|
||||
<textarea
|
||||
id="inspector-posPrompt"
|
||||
name="posPrompt"
|
||||
@input=${ this.inputChange }
|
||||
.value=${ this.posPrompt }
|
||||
></textarea>
|
||||
<label htmlFor="inspector-posPrompt">Negative Prompt</label>
|
||||
<textarea
|
||||
id="inspector-negPrompt"
|
||||
name="negPrompt"
|
||||
@input=${ this.inputChange }
|
||||
.value=${ this.negPrompt }
|
||||
></textarea>
|
||||
<sts-button
|
||||
@click=${this.compositorEnqueue(this.sceneToken)}
|
||||
>
|
||||
Generate your movie
|
||||
</sts-button>
|
||||
</section>`;
|
||||
}
|
||||
protected override render = (): TemplateResult => html`
|
||||
<section class="inspector-input-set">
|
||||
${ this.vstStyle }
|
||||
<button @click=${ this.thingy }>State</button>
|
||||
<sts-bubble-select
|
||||
.options=${STYLE_OPTIONS}
|
||||
.value=${bind(this, "vstStyle")}
|
||||
></sts-bubble-select>
|
||||
|
||||
<label htmlFor="inspector-posPrompt">Positive Prompt</label>
|
||||
<textarea
|
||||
id="inspector-posPrompt"
|
||||
name="posPrompt"
|
||||
@input=${ this.inputChange }
|
||||
.value=${ this.posPrompt }
|
||||
></textarea>
|
||||
|
||||
<label htmlFor="inspector-posPrompt">Negative Prompt</label>
|
||||
<textarea
|
||||
id="inspector-negPrompt"
|
||||
name="negPrompt"
|
||||
@input=${ this.inputChange }
|
||||
.value=${ this.negPrompt }
|
||||
></textarea>
|
||||
|
||||
<sts-button
|
||||
@click=${this.compositorEnqueue(this.sceneToken)}
|
||||
>
|
||||
Generate your movie
|
||||
</sts-button>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.studio-inspector {
|
||||
color: #fff;
|
||||
background: blue;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
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 "./bubble-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<HTMLElement>;
|
||||
|
||||
@state() sourceBlob?: Blob;
|
||||
@state() resultBlob?: Blob;
|
||||
|
||||
@state() sourceImageUrl?: string;
|
||||
@state() resultImageUrl?: string;
|
||||
|
||||
@state() selectedStyle: VstStyle = "anime_2d_flat";
|
||||
@state() prompt = "";
|
||||
@state() negPrompt = "";
|
||||
|
||||
#temporaryVideoPreviewRef = createRef<DialogElement>();
|
||||
|
||||
protected override willUpdate(changes: PropertyValues<this>): 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`
|
||||
<sts-dialog
|
||||
${ref(this.#temporaryVideoPreviewRef)}
|
||||
skipWrapper
|
||||
.trigger=${ this.trigger ?? [] }
|
||||
.template=${[this, html`
|
||||
<sts-dialog-base
|
||||
class="video-style-preview"
|
||||
@close=${() => {
|
||||
this.#temporaryVideoPreviewRef.value?.close();
|
||||
}}
|
||||
@keydown=${this.stopTextInputBubbling}
|
||||
@keyup=${this.stopTextInputBubbling}
|
||||
>
|
||||
<sts-dialog-header icon="films">
|
||||
Video Style Preview
|
||||
</sts-dialog-header>
|
||||
|
||||
<style>${unsafeCSS(styles)}</style>
|
||||
|
||||
<div class="video-style-preview__side-by-side">
|
||||
<div class="video-style-preview__panel">
|
||||
<h2 class="video-style-preview__heading">
|
||||
Raw Preview
|
||||
</h2>
|
||||
<div
|
||||
class="
|
||||
video-style-preview__image
|
||||
video-style-preview__image--source"
|
||||
style=${styleMap({
|
||||
"background-image": this.sourceImageUrl
|
||||
? `url(${this.sourceImageUrl})`
|
||||
: undefined,
|
||||
})}
|
||||
src=${ifDefined(this.sourceImageUrl)}
|
||||
>
|
||||
${!this.sourceImageUrl ? html`
|
||||
<h2 class="video-style-preview__heading">
|
||||
Loading...
|
||||
</h2>
|
||||
` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-style-preview__panel">
|
||||
<h2 class="video-style-preview__heading">
|
||||
Styled Preview
|
||||
</h2>
|
||||
<div
|
||||
class="
|
||||
video-style-preview__image
|
||||
video-style-preview__image--result"
|
||||
style=${styleMap({
|
||||
"background-image": this.resultImageUrl
|
||||
? `url(${this.resultImageUrl})`
|
||||
: undefined,
|
||||
})}
|
||||
>
|
||||
${!this.resultImageUrl ? html`
|
||||
<h2 class="video-style-preview__heading">
|
||||
Loading...
|
||||
</h2>
|
||||
` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="
|
||||
video-style-preview__form-field
|
||||
video-style-preview__form-field--base-style"
|
||||
>
|
||||
<label
|
||||
class="video-style-preview__label"
|
||||
for="video-style-preview-base-style"
|
||||
>
|
||||
Select Base Style
|
||||
</label>
|
||||
<sts-bubble-select
|
||||
id="video-style-preview-base-style"
|
||||
.options=${STYLE_OPTIONS}
|
||||
.value=${bind(this, "selectedStyle")}
|
||||
></sts-bubble-select>
|
||||
</div>
|
||||
|
||||
<div class="video-style-preview__prompts">
|
||||
<div class="video-style-preview__form-field">
|
||||
<label
|
||||
class="video-style-preview__label"
|
||||
for="video-style-preview-prompt"
|
||||
>
|
||||
Enter a Prompt
|
||||
</label>
|
||||
<textarea
|
||||
id="video-style-preview-prompt"
|
||||
class="video-style-preview__input"
|
||||
name="prompt"
|
||||
cols="60"
|
||||
rows="8"
|
||||
.value=${this.prompt}
|
||||
@input=${(event: InputEvent) => {
|
||||
this.prompt = (event.target as HTMLTextAreaElement).value;
|
||||
}}
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="video-style-preview__form-field">
|
||||
<label
|
||||
class="video-style-preview__label"
|
||||
for="video-style-preview-neg-prompt"
|
||||
>
|
||||
Negative Prompt
|
||||
</label>
|
||||
<textarea
|
||||
id="video-style-preview-neg-prompt"
|
||||
class="video-style-preview__input"
|
||||
name="neg-prompt"
|
||||
cols="60"
|
||||
rows="8"
|
||||
placeholder=${"Type here to filter out the things you don't "
|
||||
+ "want in your scene..."}
|
||||
.value=${this.negPrompt}
|
||||
@input=${(event: InputEvent) => {
|
||||
this.negPrompt = (event.target as HTMLTextAreaElement).value;
|
||||
}}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sts-dialog-footer>
|
||||
<sts-button
|
||||
primary
|
||||
icon="rotate"
|
||||
@click=${this.requestScreen}
|
||||
>
|
||||
Update
|
||||
</sts-button>
|
||||
</sts-dialog-footer>
|
||||
</sts-dialog-base>
|
||||
`] as const}
|
||||
></sts-dialog>
|
||||
`
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
background: #0006;
|
||||
background: var(--background-color, #0006);
|
||||
color: #FFFC;
|
||||
backdrop-filter: blur(16px);
|
||||
transition: {
|
||||
|
||||
@@ -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<this>): void {
|
||||
protected override update(changes: PropertyValues<this>): 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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./form-field-base.element";
|
||||
export * from "./text-field.element";
|
||||
export * from "./scalar-field.element";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<MenuPanelElement>();
|
||||
#handle?: WeakRef<OverlayHandle>;
|
||||
|
||||
#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`
|
||||
<sts-menu-panel
|
||||
${ref(this.#panelRef)}
|
||||
id=${this.#menuId}
|
||||
@@ -108,7 +109,9 @@ export class MenuElement extends LitElement {
|
||||
// <sts-menu-panel>'s slot
|
||||
this.#slotRef.value?.assignedNodes()
|
||||
}</sts-menu-panel>
|
||||
`);
|
||||
`;
|
||||
|
||||
this.#handle = this._overlay.attach(this, this.#template);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
@@ -134,8 +137,9 @@ export class MenuElement extends LitElement {
|
||||
protected override update(changes: PropertyValues<this>): 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<this>): void {
|
||||
|
||||
@@ -83,7 +83,7 @@ export interface OverlayProvider {
|
||||
detach(handle: WeakRef<OverlayHandle>): void;
|
||||
|
||||
/** Re-render a currently-attached overlay. */
|
||||
reRender(handle: WeakRef<OverlayHandle>): void;
|
||||
reRender(handle: WeakRef<OverlayHandle>, 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<OverlayHandle>): void {
|
||||
reRender(handle: WeakRef<OverlayHandle>, 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 {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user