UI styling, main workspace layout (#119)

This commit is contained in:
Danny McGee
2024-04-05 20:15:09 -04:00
committed by GitHub
parent c5a0250d9f
commit bfc638b644
57 changed files with 2004 additions and 694 deletions
+1
View File
@@ -1,3 +1,4 @@
export * from "./decorators";
export * from "./events";
export * from "./observe";
export * from "./types";
+79
View File
@@ -0,0 +1,79 @@
import { LitElement, PropertyDeclaration, PropertyValues, html, render } from "lit";
import { customElement, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { inject, provide } from "./decorators";
import { observe } from "./observe";
import { DynamicProvider, UniqueToken } from "./types";
interface CountProvider extends DynamicProvider<CountProvider> {
readonly count: number;
}
const COUNT_PROVIDER = UniqueToken.create<CountProvider>();
@customElement("test-count-provider")
@provide(COUNT_PROVIDER)
class TestCountProvider extends LitElement implements CountProvider {
@state() count = 0;
protected override update(changes: PropertyValues<this>): void {
if (changes.has("count")) {
const downstreamChanges: PropertyValues<CountProvider> = new Map();
downstreamChanges.set("count", changes.get("count"));
this.dispatchEvent(new CustomEvent("property-changes", {
detail: downstreamChanges,
}));
}
super.update(changes);
}
protected override render = () => html`
<slot></slot>
`;
}
@customElement("test-count-dependent")
class TestCountDependent extends LitElement {
@observe(["count"])
@inject(COUNT_PROVIDER)
countProvider!: CountProvider;
override requestUpdate(
name?: PropertyKey | undefined,
oldValue?: unknown,
options?: PropertyDeclaration<unknown, unknown> | undefined
): void {
super.requestUpdate(name, oldValue, options);
}
protected override update(changes: PropertyValues<this>): void {
this.setAttribute("count", this.countProvider?.count?.toString() ?? null);
super.update(changes);
}
}
describe("@observe", () => {
it("should work", async () => {
const providerRef = createRef<TestCountProvider>();
const dependentRef = createRef<TestCountDependent>();
render(html`
<test-count-provider ${ref(providerRef)}>
<test-count-dependent ${ref(dependentRef)}></test-count-dependent>
</test-count-provider>
`, document.body);
expect(providerRef.value).toBeDefined();
expect(dependentRef.value).toBeDefined();
while (!(await dependentRef.value!.updateComplete));
expect(dependentRef.value!.getAttribute("count")).toEqual("0");
providerRef.value!.count = 42;
while (!(await providerRef.value!.updateComplete));
while (!(await dependentRef.value!.updateComplete));
expect(dependentRef.value!.getAttribute("count")).toEqual("42");
});
});
+80
View File
@@ -0,0 +1,80 @@
import { LitElement, PropertyValues } from "lit";
import { appendRoutine, prependRoutine } from "../decorators/internal";
import { DynamicProvider } from "./types";
/**
* Observe the given property names of the decorated {@linkcode DynamicProvider},
* calling `requestUpdate` to re-render the component when one of those
* properties is updated in the provider.
*
* @example
* ```ts
* const COUNT_PROVIDER = UniqueToken.create<CountProvider>();
*
* interface CountProvider extends DynamicProvider<CountProvider> {
* readonly count: number;
* }
*
* \@customElement("sts-count-observer")
* export class CountObserverElement extends LitElement {
* \@observe(["count"])
* \@inject(COUNT_PROVIDER)
* _countProvider!: CountProvider;
*
* protected override render = () => html`
* <!--
* This component should correctly update whenever the injected
* `CountProvider`'s `count` property changes.
* -->
* The count is: ${this._countProvider.count}
* `;
* }
* ```
*/
export function observe<
T extends LitElement,
K extends keyof T,
P extends (T[K] & object),
>(observedProps: (keyof P)[])
: T[K] extends DynamicProvider<T[K]>
? ((proto: T, propName: K) => void)
: never
{
return ((proto: T, propName: K) => {
function onProviderChanges(
this: T,
{ detail: changes }: CustomEvent<PropertyValues<P>>,
): void {
// FIXME: Holy shit TypeScript, gg
if (observedProps.some(prop => changes.has(prop as never)))
this.requestUpdate();
}
const $onProviderChanges = Symbol("onProviderChanges");
type Decorated = T & {
[$onProviderChanges]: typeof onProviderChanges;
}
appendRoutine(proto, "connectedCallback", function (this: T) {
assertType<Decorated>(this);
this[$onProviderChanges] ??= onProviderChanges.bind(this);
const provider = this[propName];
assertType<DynamicProvider<P>>(provider);
provider?.addEventListener?.("property-changes", this[$onProviderChanges]);
});
prependRoutine(proto, "disconnectedCallback", function (this: T) {
assertType<Decorated>(this);
const provider = this[propName];
assertType<DynamicProvider<P>>(provider);
provider?.removeEventListener?.("property-changes", this[$onProviderChanges]);
});
}) as any
}
function assertType<T>(value: unknown): asserts value is T {}
+1 -1
View File
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/src/styles.scss" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body data-sl="canvas-mq">
<app-root></app-root>
@@ -23,7 +23,6 @@
.timeline-wrapper {
grid-area: timeline;
display: grid;
border-radius: 8px;
background: var(--color-panel-background);
grid-template-columns:
[sidebar-start] min-content [sidebar-end
@@ -146,7 +145,7 @@
grid-template-columns: subgrid;
grid-row: 2 / span var(--track-count);
.track {
&:hover {
background: #0000001F;
@@ -162,4 +161,4 @@
}
}
}
}
}
+54 -50
View File
@@ -1,7 +1,7 @@
import * as studio from "@storyteller/studio";
import type { EntitySelectEvent, SeekTimeChanged, UpdateAnimationTargets, Keyframe } from "@storyteller/studio";
import type { EntitySelectEvent, SeekTimeChanged, UpdateAnimationTargets, Keyframe, AnimationTrack } from "@storyteller/studio";
import { bind, drag, inject, on, provide } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, unsafeCSS, TemplateResult, PropertyValueMap } from "lit";
import { LitElement, type PropertyValues, html, unsafeCSS, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
@@ -27,6 +27,8 @@ function isOutsideEntity(
return "outsideEntity" in value;
}
// TODO: Consider refactoring to get/set most of the `@state` here through
// `AnimTimelineController`
@customElement("sts-anim-editor")
@provide(TREE_SELECTION_PROVIDER)
export class AnimEditorElement extends LitElement
@@ -47,7 +49,7 @@ export class AnimEditorElement extends LitElement
@state() playHead = 0;
@state() playing = false;
@state() tracks: Record<string, { name?: string, target_type: studio.SceneElement, animation_target: studio.AnimationTarget }> = {};
@state() tracks: Record<string, AnimationTrack> = {};
@state() track_ordered: [string, studio.AnimationTarget][] = [];
@@ -115,7 +117,7 @@ export class AnimEditorElement extends LitElement
delete tracks[track_entity];
}
this.track_ordered = Object.keys(tracks).map(entity => [entity, tracks[entity].animation_target]);
this.track_ordered = Object.keys(tracks).map(entity => [entity, tracks[entity].animationTarget]);
if (typeof this.selectedTrack === "object") {
if (!isOutsideEntity(this.selectedTrack) && !(this.selectedTrack.track in tracks)) {
@@ -309,44 +311,45 @@ export class AnimEditorElement extends LitElement
let timeline_css_variables = {
'--track-count': `${this.trackCount()}`,
'--row-count': `calc(var(--track-count) + ${canAddSelected ? 3 : 2})`,
'--row-count': `calc(var(--track-count) + ${/*canAddSelected ? 3 :*/ 2})`,
'--clip-end': `${clip_end * 100}%`,
'--clip-start': `${clip_start * 100}%`
};
const tracklist = this.track_ordered.map(([entity,]) => {
let track = this.tracks[entity];
let name = track.name ?? track.target_type;
let name = track.name ?? track.targetType;
let icon = this.sceneData?.entityMap.get(entity)?.icon ?? "minus";
return html`
<sts-tree class="track"
role="listitem"
name=${name}
icon=${icon}
@tree-select=${() => {
studio.select(entity);
}}
.id=${entity}
></sts-tree>
<sts-tree class="track"
role="listitem"
name=${name}
icon=${icon}
@tree-select=${() => {
studio.select(entity);
}}
.id=${entity}
></sts-tree>
`;
});
if (canAddSelected) {
tracklist.push(html`
<sts-tree class=${`track`}
role="button"
.name=${`Add Track: ${this.sceneData?.selectedObject?.name ?? canAddSelected}`}
icon="plus-large"
@click=${this.addTarget}></sts-tree>
`);
}
// if (canAddSelected) {
// tracklist.push(html`
// <sts-tree class=${`track`}
// role="button"
// .name=${`Add Track: ${this.sceneData?.selectedObject?.name ?? canAddSelected}`}
// icon="plus-large"
// @click=${this.addTarget}></sts-tree>
// `);
// }
const none_selected = !selected;
return html`
<!--
<div class="timeline-controls">
<sts-toolbar>
<sts-toolbar-group>
<sts-toolbar-group>
${this.keyingMode === "automatic" ? html`
<sts-button class="btn-autokey active"
icon="record"
@@ -404,35 +407,36 @@ export class AnimEditorElement extends LitElement
</sts-toolbar-group>
</sts-toolbar>
</div>
-->
<div class="timeline-wrapper">
<div class="sidebar"
style=${styleMap({
width: `${this.sidebarWidth}px`,
...timeline_css_variables
})}
>
<div class="tracklist" role="list">
${tracklist}
<div class="sidebar"
style=${styleMap({
width: `${this.sidebarWidth}px`,
...timeline_css_variables
})}
>
<div class="tracklist" role="list">
${tracklist}
</div>
</div>
</div>
<div class="divider" style=${styleMap(timeline_css_variables)}
${drag({
move: this.onSidebarResize,
})}
></div>
<div class="divider" style=${styleMap(timeline_css_variables)}
${drag({
move: this.onSidebarResize,
})}
></div>
<sts-anim-timeline class="timeline"
style=${styleMap(timeline_css_variables)}
.rangeStart=${bind(this, "timelineStart")}
.rangeEnd=${bind(this, "timelineEnd")}
.clipStart=${this.clipStart}
.clipEnd=${this.clipEnd}
.playHead=${this.playHead}
.keyframes=${bind(this, "keyframes")}
.track_ordered=${track_order_hashed}
.track_animation_target_ordered=${track_animation_target_order_hashed}
></sts-anim-timeline>
<sts-anim-timeline class="timeline"
style=${styleMap(timeline_css_variables)}
.rangeStart=${bind(this, "timelineStart")}
.rangeEnd=${bind(this, "timelineEnd")}
.clipStart=${this.clipStart}
.clipEnd=${this.clipEnd}
.playHead=${this.playHead}
.keyframes=${bind(this, "keyframes")}
.track_ordered=${track_order_hashed}
.track_animation_target_ordered=${track_animation_target_order_hashed}
></sts-anim-timeline>
</div>
`
};
@@ -0,0 +1,3 @@
:host {
display: contents;
}
@@ -0,0 +1,228 @@
import { DynamicProvider, UniqueToken, inject, on, provide } from "@storyteller/framework";
import * as studio from "@storyteller/studio";
import {
type AnimationTarget,
type AnimationTrack,
type Keyframe,
type SeekTimeChanged,
type UpdateAnimationTargets,
type UpdateKeyframes,
} from "@storyteller/studio";
import {
SCENE_DATA_PROVIDER,
type SceneDataProvider,
} from "@storyteller/studio-web/scene-data-provider";
import { LitElement, PropertyValues, html, unsafeCSS } from "lit";
import { customElement, state } from "lit/decorators.js";
import styles from "./anim-timeline-controller.element.scss?inline";
export const ANIM_TIMELINE_CONTROLLER = UniqueToken.create<AnimTimelineController>();
export interface AnimTimelineController extends DynamicProvider<AnimTimelineController> {
readonly seekTime: number;
readonly playing: boolean;
readonly tracks: Record<Entity, AnimationTrack>;
readonly tracksOrdered: [Entity, AnimationTarget][];
readonly keyframes: Record<Entity, Keyframe>;
readonly clipStart: number;
readonly clipEnd: number;
setSeekTime(time: number): void;
addTrack(entity: Entity): void;
removeTrack(entity: Entity): void;
addKeyframe(): void;
togglePlaying(): void;
play(): void;
pause(): void;
jumpToStart(): void;
jumpToEnd(): void;
}
export type Entity = string;
@customElement("sts-anim-timeline-controller")
@provide(ANIM_TIMELINE_CONTROLLER)
export class AnimTimelineControllerElement
extends LitElement
implements AnimTimelineController
{
static override styles = unsafeCSS(styles);
@state() seekTime = 0;
@state() playing = false;
@state() tracks: Record<Entity, AnimationTrack> = {};
@state() tracksOrdered: [Entity, AnimationTarget][] = [];
@state() selectedTrack: AnimationTrack | null = null;
@state() keyframes: Record<Entity, Keyframe> = {};
@state() clipStart = 0;
@state() clipEnd = 0;
@inject(SCENE_DATA_PROVIDER)
_sceneData!: SceneDataProvider;
setSeekTime(time: number): void {
studio.seekTime(time);
}
addTrack(entity: Entity): void {
if (!(entity in this.tracks))
studio.addEntityTrack(entity);
}
removeTrack(entity: Entity): void {
this.tracks = Object.fromEntries(
Object.entries(this.tracks).filter(([key]) => key !== entity)
)
}
addKeyframe(): void {
const selected = this._sceneData.selectedEntity;
if (selected != null && selected in this.tracks) {
studio.recordKeyframe([selected]);
}
}
togglePlaying(): void {
if (this.playing) studio.pause();
else studio.play();
}
play(): void {
studio.play();
}
pause(): void {
studio.pause();
}
jumpToStart(): void {
if (Math.abs(this.seekTime - this.clipStart) < 0.0001) {
studio.seekTime(0);
} else {
studio.seekTime(this.clipStart);
}
}
jumpToEnd(): void {
studio.seekTime(this.clipEnd);
}
override connectedCallback(): void {
this._sceneData?.addEventListener("property-changes", this._onSceneDataUpdated);
super.connectedCallback();
}
override disconnectedCallback(): void {
this._sceneData?.removeEventListener("property-changes", this._onSceneDataUpdated);
super.disconnectedCallback()
}
protected override willUpdate(changes: PropertyValues<this>): void {
if (changes.has("tracks")) {
this.tracksOrdered = Object
.entries(this.tracks)
.map(([entity, track]) => [entity, track.animationTarget]);
}
if (changes.has("keyframes")) {
let min = Infinity
let max = -Infinity;
const keyframeKeys = Object.keys(this.keyframes);
if (!keyframeKeys.length) {
this.clipStart = 0;
this.clipEnd = 0;
return;
}
for (const keyframe of keyframeKeys) {
const current = this.keyframes[keyframe];
let time = current.time;
if (typeof time != "number") {
continue;
}
if (time > max) {
max = time;
}
if (time < min) {
min = time;
}
if ("duration" in current) {
const endTime = time + current.duration;
if (endTime > max) {
max = endTime
}
}
}
this.clipStart = Math.max(min, 0.);
this.clipEnd = Math.max(max, min);
}
super.willUpdate(changes);
}
protected override update(changes: PropertyValues<this>): void {
this.dispatchEvent(new CustomEvent("property-changes", {
detail: new Map(changes.entries())
}));
super.update(changes);
}
_onSceneDataUpdated = ({ detail: changes }: CustomEvent<PropertyValues<SceneDataProvider>>) => {
if (changes.has("selectedEntity")) {
const selected = this._sceneData.selectedEntity;
if (selected) {
this.selectedTrack = this.tracks[selected] ?? null;
} else {
this.selectedTrack = null;
}
}
}
@on("window:seek-time-changed")
_onSeekTimeChanged({ detail: { time, playing }}: SeekTimeChanged): void {
this.seekTime = time;
this.playing = playing;
}
@on("window:update-animation-targets")
_onUpdateAnimationTargets({ detail: { added, removed }}: UpdateAnimationTargets): void {
if (Object.keys(added).length === 0 && removed.length === 0)
return;
const updated = {
...this.tracks,
...added,
};
for (let entity of removed)
delete updated[entity];
this.tracks = updated;
console.log("tracks:", this.tracks);
}
@on("window:update-keyframes")
_onUpdateKeyframes({ detail: { added, removed }}: UpdateKeyframes): void {
if (Object.keys(added).length === 0 && removed.length === 0)
return;
const updated = {
...this.keyframes,
...added,
};
for (let key of removed)
delete updated[key];
this.keyframes = updated;
}
protected override render = () => html`
<slot></slot>
`;
}
@@ -0,0 +1,28 @@
.play-pause {
align-self: center;
padding: 0.4rem;
border-radius: 50%;
background: #FFF;
color: var(--neutral-100);
&::part(icon) {
width: 1em;
font-size: 0.75rem;
}
}
.timecode {
display: block;
align-self: center;
margin-inline: 0.5rem;
color: rgba(#FFF, 0.5);
font: {
size: 0.875rem;
weight: 500;
variant-numeric: tabular-nums;
}
strong {
color: #FFF;
}
}
@@ -0,0 +1,63 @@
import { inject, observe } from "@storyteller/framework";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement } from "lit/decorators.js";
import {
ANIM_TIMELINE_CONTROLLER,
type AnimTimelineController,
} from "./anim-timeline-controller.element";
import styles from "./anim-timeline-controls.element.scss?inline";
@customElement("sts-anim-timeline-controls")
export class AnimTimelineControlsElement extends LitElement {
static override styles = unsafeCSS(styles);
@observe(["seekTime", "clipEnd", "playing"])
@inject(ANIM_TIMELINE_CONTROLLER)
_timeline!: AnimTimelineController;
timestamp(fractionalSeconds: number): string {
function modf(num: number, divisor: number): [number, number] {
return [Math.floor(num / divisor), Math.floor(num % divisor)];
}
const totalMs = Math.round(fractionalSeconds * 1000);
const [totalSeconds, ms] = modf(totalMs, 1000);
const [minutes, seconds] = modf(totalSeconds, 60);
return `${
minutes.toString().padStart(2, "0")
}:${
seconds.toString().padStart(2, "0")
}.${
ms.toString().padStart(3, "0")
}`
}
protected override render = () => html`
<sts-toolbar>
<sts-toolbar-group>
<sts-button
icon="bwd-step"
@click=${() => this._timeline.jumpToStart()}
></sts-button>
<sts-button
class="play-pause"
.icon=${this._timeline.playing ? "pause" : "play"}
@click=${() => this._timeline.togglePlaying()}
></sts-button>
<sts-button
icon="fwd-step"
@click=${() => this._timeline.jumpToEnd()}
></sts-button>
<span class="timecode">
<strong>${this.timestamp(this._timeline.seekTime)}</strong>
/ ${this.timestamp(this._timeline.clipEnd)}
</span>
</sts-toolbar-group>
</sts-toolbar>
`;
}
@@ -64,7 +64,6 @@
}
user-select: none;
pointer-events: none;
z-index: 1;
.frac {
color: #FFF3;
+24 -118
View File
@@ -3,94 +3,32 @@
$hierarchy-width: 290px;
.app-root {
position: fixed;
inset: 0;
--sidebar-width: 300px;
--timeline-height: max-content;
}
--timeline-height: min-content;
.debug-mode {
.studio-scene {
z-index: 100;
}
}
.wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: grid;
pointer-events: none;
grid-template-columns: [timeline-start screen-start] 2rem [left-toolbar-start help-start] max-content [left-toolbar-end] auto [center-toolbar-start] max-content [center-toolbar-end] auto [right-toolbar-start] max-content [right-toolbar-end] 2rem [screen-end help-end sidebar-start] var(--sidebar-width) [sidebar-end timeline-end];
grid-template-rows: [top-toolbar-start sidebar-start screen-start] min-content [top-toolbar-end help-start sidebar-view-start] max-content [help-end] auto [bottom-toolbar-start] max-content [bottom-toolbar-end sidebar-end sidebar-view-end screen-end timeline-start] var(--timeline-height) [timeline-end];
.studio-scene {
position: fixed;
bottom: 0;
left: 0;
right: 0;
pointer-events: all;
width: 100vw;
height: 100vh;
grid-template: {
areas:
"viewport sidebar"
"timeline timeline";
rows: minmax(0, 1fr) var(--timeline-height);
columns: minmax(0, 1fr) var(--sidebar-width);
}
.action-toolbar {
grid-column: left-toolbar;
grid-row: top-toolbar;
padding-top: var(--padding);
sts-toolbar-group {
gap: var(--padding);
}
sts-button {
background-color: var(--color-toolbar-background);
}
z-index: 1;
}
.timeline {
grid-row-start: bottom-toolbar-start;
grid-row-end: timeline-end;
grid-column: timeline;
z-index: 1;
border-radius: 0;
}
.viewport { grid-area: viewport; }
.sidebar { grid-area: sidebar; }
.timeline { grid-area: timeline; }
.sidebar {
display: grid;
grid-area: sidebar;
grid-template-rows: subgrid;
z-index: 1;
display: flex;
flex-direction: column;
background-color: var(--color-panel-background);
border-radius: 0;
position: relative;
padding: var(--padding);
padding-left: 0.5rem;
.resize-handle {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0.5rem;
background: #0004;
transition: 66.666ms background linear;
&:hover {
background: #FFF1;
}
}
.sidebar-header {
grid-row: top-toolbar;
display: flex;
flex-direction: row;
justify-content: stretch;
gap: 0;
.sidebar-tab {
@@ -108,51 +46,19 @@ $hierarchy-width: 290px;
}
.sidebar-view {
grid-row: sidebar-view;
}
}
.help {
grid-area: help;
z-index: 1;
display: flex;
flex-direction: row;
justify-content: start;
}
.entity-inspector {
grid-column: left-toolbar;
grid-row: bottom-toolbar;
z-index: 1;
padding-bottom: var(--padding);
& > * {
pointer-events: all;
}
}
.transform-toolbar {
grid-column: center-toolbar;
grid-row: top-toolbar;
background-color: var(--color-toolbar-background);
z-index: 1;
border-top-right-radius: 0;
border-top-left-radius: 0;
border-bottom-right-radius: var(--corner-radius);
border-bottom-left-radius: var(--corner-radius);
padding: var(--padding);
display: flex;
flex-direction: row;
gap: 1rem;
.add-to-scene-dropdown {
background-color: var(--color-toolbar-background-light);
font-size: 16px;
padding: 10px 12px;
padding: 1rem;
}
}
}
.studio-scene.block-pointer-events::part(canvas) {
.help {
grid-area: help;
display: flex;
flex-direction: row;
justify-content: start;
}
.studio.block-pointer-events::part(canvas) {
pointer-events: none;
}
@@ -215,4 +121,4 @@ $hierarchy-width: 290px;
.menu-button-icon {
font-size: 12px;
opacity: 0.75;
}
}
+73 -132
View File
@@ -1,14 +1,14 @@
import { drag, on, provide } from "@storyteller/framework";
import * as studio from "@storyteller/studio";
import {
type SceneSavedEvent,
type SceneStateEvent,
SceneState,
StudioMode,
type DebugModeToggled,
} from "@storyteller/studio";
import { ButtonElement } from "@storyteller/studio-ui/button";
import { DialogElement } from "@storyteller/studio-ui/dialog";
import {
STUDIO_PARAMS,
SceneHierarchyElement,
StorytellerApi,
StorytellerLocalApi,
@@ -18,10 +18,9 @@ import {
} from "@storyteller/studio-web";
import { match } from "@storyteller/utility";
import { LitElement, TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { createRef, ref } from "lit/directives/ref.js";
import { styleMap } from "lit/directives/style-map.js";
import "@storyteller/studio-ui/button";
import "@storyteller/studio-ui/dialog";
@@ -39,7 +38,9 @@ import "@storyteller/studio-web/scene-hierarchy";
import "@storyteller/studio-web/studio";
import "@storyteller/studio-web/transform-toolbar";
import "./anim-editor.element";
import "./anim-timeline-controller.element";
import "./help.element";
import "./studio-viewport.element";
import "./temp-video-preview.element";
import "./app.element.scss";
@@ -49,12 +50,16 @@ import "./app.element.scss";
token: StorytellerApi,
provide() { return this.api; }
})
@provide({
token: STUDIO_PARAMS,
provide() { return this.params; }
})
export class AppElement extends LitElement {
@state() showHelp = false;
@state() loading = true;
@state() resizingPanel = false;
@state() inspectorWidth = 320;
// @state() resizingPanel = false;
// @state() inspectorWidth = 320;
@state() debugMode = false;
@@ -62,16 +67,13 @@ export class AppElement extends LitElement {
@state() currentSidebar : string = 'hierarchy';
@property({ attribute: false }) api: StorytellerApi = new StorytellerApi(mockApi ? new StorytellerLocalApi() : new StorytellerRemoteApi());
api = new StorytellerApi(mockApi ? new StorytellerLocalApi() : new StorytellerRemoteApi());
params = new StudioURLParams();
#addToSceneButtonRef = createRef<ButtonElement>();
#videoStylePreviewBtnRef = createRef<ButtonElement>();
#mediaLibraryRef = createRef<DialogElement>();
#inspectorRef = createRef<HTMLDivElement>();
#hierarchyRef = createRef<SceneHierarchyElement>();
#params = new StudioURLParams();
protected override createRenderRoot(): Element | ShadowRoot {
return this;
}
@@ -80,7 +82,7 @@ export class AppElement extends LitElement {
async onSceneSaved({ detail: scene }: SceneSavedEvent): Promise<void> {
try {
const file = new File([scene], "scene.scn.ron", { type: "text/ron" });
const token = await this.api.createScene(file);
this.dispatchEvent(new CustomEvent("scene-uploaded", {
@@ -88,7 +90,7 @@ export class AppElement extends LitElement {
bubbles: true,
composed: true,
}));
}
catch (err) {
console.error(err);
@@ -106,22 +108,18 @@ export class AppElement extends LitElement {
}
@on("window:debug-mode-toggled")
onDebugModeToggled({detail: { debugModeActive }}: studio.DebugModeToggled ): void {
onDebugModeToggled({detail: { debugModeActive }}: DebugModeToggled ): void {
this.debugMode = debugModeActive;
}
onPanelResizeStart(): void {
this.resizingPanel = true;
}
// onPanelResizeEnd(): void {
// this.resizingPanel = false;
// }
onPanelResizeEnd(): void {
this.resizingPanel = false;
}
resizeInspectorWidth(event: PointerEvent): void {
const updated = this.inspectorWidth - event.movementX;
this.inspectorWidth = Math.max(Math.min(updated, 1024), 128);
}
// resizeInspectorWidth(event: PointerEvent): void {
// const updated = this.inspectorWidth - event.movementX;
// this.inspectorWidth = Math.max(Math.min(updated, 1024), 128);
// }
toggleHelp(): void {
this.showHelp = !this.showHelp;
@@ -140,134 +138,77 @@ export class AppElement extends LitElement {
protected override render() {
return html`
<sts-canvas-provider class=${classMap({"app-root": true, "debug-mode": this.debugMode})}>
<sts-canvas-provider>
<sts-scene-data-provider>
<sts-overlay-provider>
<sts-anim-timeline-controller>
<sts-overlay-provider
class=${classMap({
"app-root": true,
"debug-mode": this.debugMode,
})}
>
<sts-iframe-messaging-bridge></sts-iframe-messaging-bridge>
${this.#params.mode === StudioMode.Editor ? html`
<sts-add-to-scene-menu
.trigger=${this.#addToSceneButtonRef}
@open-media-library=${() => {
this.#mediaLibraryRef.value?.open();
}}
></sts-add-to-scene-menu>
<sts-dialog
${ref(this.#mediaLibraryRef)}
tag="sts-media-library-dialog"
nonBlocking
></sts-dialog>
` : html`<></>`}
<div class="wrapper" style=${styleMap({ "--sidebar-width": `${this.inspectorWidth}px` })}>
<!-- Scene view -->
<sts-studio
class=${classMap({
"studio-scene": true,
"block-pointer-events": this.resizingPanel,
})}
.objectId=${this.#params.objectId}
.bvhPath=${this.#params.bvh}
.mixamoPath=${this.#params.mixamo}
.sceneImport=${this.#params.sceneImport}
.storytellerScene=${this.#params.scene}
.skyboxId=${this.#params.skybox ?? "test_scene"}
.mode=${this.#params.mode}
></sts-studio>
<!-- Actions toolbar -->
<sts-toolbar class="action-toolbar">
<sts-toolbar-group>
${this.#params.mode === StudioMode.Editor ? html`
<sts-button
@click=${() => studio.saveScene()}
>
Save Scene
</sts-button>
` : nothing}
<sts-button
class=${classMap({ active: this.showHelp })}
@click=${this.toggleHelp}
>
Help
</sts-button>
</sts-toolbar-group>
</sts-toolbar>
<sts-studio-viewport class="viewport"></sts-studio-viewport>
<!-- Studio-mode-only elements -->
${this.#params.mode === StudioMode.Editor ? html`
<div class="transform-toolbar">
<sts-button
${ref(this.#addToSceneButtonRef)}
icon="plus-large"
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>
<section
${ref(this.#inspectorRef)}
class="sidebar"
style=${styleMap({
width: `${this.inspectorWidth}px`,
})}
aria-labelledby="sidebar-heading"
>
<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()}
<div
class="resize-handle"
${drag({
start: this.onPanelResizeStart,
end: this.onPanelResizeEnd,
move: this.resizeInspectorWidth,
})}
></div>
</section>
<div class="entity-inspector">
<sts-entity-inspector></sts-entity-inspector>
${this.params.mode === StudioMode.Editor ? html`
<section
${ref(this.#inspectorRef)}
class="sidebar"
aria-labelledby="sidebar-heading"
>
<div class="sidebar-header">
${this.sidebars.map(({title, name}) => html`
<sts-button
class=${classMap({
"current-tab": name === this.currentSidebar,
"sidebar-tab": true
})}
>
${title}
</sts-button>
`)}
</div>
<sts-anim-editor
class="timeline"
style=${styleMap({
right: `${this.inspectorWidth + 32}px`
})}
></sts-anim-editor>
${this.renderSidebar()}
<!-- <div
class="resize-handle"
${/*drag({
start: this.onPanelResizeStart,
end: this.onPanelResizeEnd,
move: this.resizeInspectorWidth,
})*/ nothing}
></div> -->
</section>
<!-- TODO
<div class="entity-inspector">
<sts-entity-inspector></sts-entity-inspector>
</div>
-->
<sts-anim-editor class="timeline"></sts-anim-editor>
` : nothing}
<!-- Dialogs and modals -->
<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 class="help">
<sts-help></sts-help>
</div>
` : nothing}
</div>
${this.loading ? html`
<sts-loading></sts-loading>
` : nothing}
</sts-overlay-provider>
</sts-anim-timeline-controller>
</sts-scene-data-provider>
</sts-canvas-provider>
`;
@@ -0,0 +1,152 @@
@use "sass:math";
sts-studio-viewport {
background: var(--neutral-100);
.studio {
position: relative;
.debug-mode & :not([slot="canvas"]) {
display: none;
}
}
.stylize {
gap: 0.5rem;
padding: 0.5rem 0.75rem;
line-height: 1;
sts-icon {
align-self: center;
}
}
.timeline-controls {
background: var(--neutral-200);
border-radius: 0.5rem;
}
sts-toolbar,
.timeline-controls {
&.v-start {
border: {
top-left-radius: 0;
top-right-radius: 0;
}
}
&.v-end {
border: {
bottom-left-radius: 0;
bottom-right-radius: 0;
}
}
}
.abs {
position: absolute;
}
.h-start {
left: 0;
&.offset {
left: 1rem;
}
}
.h-end {
right: 0;
&.offset {
right: 1rem;
}
}
.v-start {
top: 0;
&.offset {
top: 1rem;
}
}
.v-end {
bottom: 0;
&.offset {
bottom: 1rem;
}
}
.h-center {
left: 50%;
transform: translateX(-50%);
}
.primitive::part(icon) {
font-size: 1.125rem;
}
.transform-space {
margin-inline-end: 0.75rem;
}
.inspector {
min-width: math.div(330, 16) * 1rem;
padding: 1rem;
border: 1px solid var(--neutral-300);
border-radius: 0.5rem;
background: var(--neutral-100);
&__header {
display: flex;
align-items: center;
gap: 1.5rem;
margin: 0 0 1.25rem;
color: #FFF;
.object-name {
display: flex;
align-items: center;
gap: 0.5rem;
h3 {
margin: 0;
font: {
size: 1rem;
weight: 700;
}
}
}
.swap-object {
padding: 0;
gap: 0.375rem;
margin-inline-start: auto;
background: transparent;
color: rgba(#FFF, 0.6);
font-size: 0.75rem;
&:hover {
color: #FFF;
}
}
}
&__footer {
display: flex;
gap: 0.5rem;
margin: 1.75rem 0 0;
.add-keyframe,
.add-to-timeline {
flex: 1 0 0;
}
.add-to-timeline {
justify-content: center;
}
.delete {
padding-inline: 0.8rem;
}
}
}
}
@@ -0,0 +1,288 @@
import { bind, inject, observe } from "@storyteller/framework";
import * as studio from "@storyteller/studio";
import { BlockingCommand, TransformSpace, TransformType } from "@storyteller/studio";
import {
SCENE_DATA_PROVIDER,
STUDIO_PARAMS,
type SceneDataProvider,
type StudioParams,
} from "@storyteller/studio-web";
import { ButtonElement } from "@storyteller/studio-ui/button";
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import {
ANIM_TIMELINE_CONTROLLER,
type AnimTimelineController,
} from "./anim-timeline-controller.element";
import "@storyteller/studio-web/entity-inspector";
import "@storyteller/studio-web/studio";
import "@storyteller/studio-ui/button";
import "@storyteller/studio-ui/forms/radio";
import "@storyteller/studio-ui/forms/select";
import "@storyteller/studio-ui/toolbar";
import "./anim-timeline-controls.element";
import "./studio-viewport.element.scss";
@customElement("sts-studio-viewport")
export class StudioViewportElement extends LitElement {
@inject(STUDIO_PARAMS)
_params!: StudioParams;
@observe(["selectedEntity", "selectedObject"])
@inject(SCENE_DATA_PROVIDER)
_sceneData!: SceneDataProvider;
@observe(["tracks"])
@inject(ANIM_TIMELINE_CONTROLLER)
_timeline!: AnimTimelineController;
@state() _transformSpace = TransformSpace.Local;
@state() _transformType = TransformType.Translate;
get #selectedEntityInTracklist() {
return (
this._sceneData.selectedEntity != null
&& this._sceneData.selectedEntity in this._timeline.tracks
);
}
#fileTriggerRef = createRef<ButtonElement>();
#mediaLibraryTriggerRef = createRef<ButtonElement>();
get #selectedObject() {
return this._sceneData.selectedObject;
}
protected override createRenderRoot = () => this;
protected override update(changes: PropertyValues<this>): void {
if (changes.has("_transformSpace"))
studio.setTransformSpace(this._transformSpace);
if (changes.has("_transformType"))
studio.setTransformType(this._transformType);
super.update(changes);
}
protected override render(): TemplateResult {
return html`
<sts-studio
class="studio"
.objectId=${this._params.objectId}
.bvhPath=${this._params.bvh}
.mixamoPath=${this._params.mixamo}
.sceneImport=${this._params.sceneImport}
.storytellerScene=${this._params.scene}
.skyboxId=${this._params.skybox ?? "test_scene"}
.mode=${this._params.mode}
>
<div class="main-menu abs h-start v-start offset">
<sts-button
${ref(this.#fileTriggerRef)}
dropdown
icon="file"
>
File
</sts-button>
<sts-button
icon="question"
@click=${() => {
// TODO
}}
>
Help
</sts-button>
</div>
<sts-toolbar class="top-toolbar abs h-center v-start">
<sts-toolbar-group>
<sts-button
${ref(this.#mediaLibraryTriggerRef)}
secondary
icon="plus-large"
title="Add asset from Media Library"
></sts-button>
</sts-toolbar-group>
<hr />
<sts-toolbar-group>
<sts-button
class="primitive"
icon="cube"
title="Cube"
@click=${() => studio.dispatchBlockingCommand(BlockingCommand.Cube)}
></sts-button>
<sts-button
class="primitive"
icon="cylinder"
title="Cylinder"
@click=${() => studio.dispatchBlockingCommand(BlockingCommand.Cylinder)}
></sts-button>
<sts-button
class="primitive"
icon="sphere"
title="Sphere"
@click=${() => studio.dispatchBlockingCommand(BlockingCommand.Sphere)}
></sts-button>
<sts-button
class="primitive"
icon="torus"
title="Torus"
@click=${() => studio.dispatchBlockingCommand(BlockingCommand.Torus)}
></sts-button>
</sts-toolbar-group>
<hr />
<sts-toolbar-group>
<sts-select class="transform-space"
title="Transform space"
.value=${bind(this, "_transformSpace")}
>
<sts-option .value=${TransformSpace.Local}>
<sts-icon icon="xform-local"></sts-icon>
Local
</sts-option>
<sts-option .value=${TransformSpace.World}>
<sts-icon icon="globe"></sts-icon>
Global
</sts-option>
</sts-select>
<sts-radio-group
title="Transform type"
.value=${bind(this, "_transformType")}
>
<sts-radio .value=${TransformType.Translate}>
<sts-icon icon="up-down-left-right">Translate</sts-icon>
</sts-radio>
<sts-radio .value=${TransformType.Rotate}>
<sts-icon icon="rotate">Rotate</sts-icon>
</sts-radio>
<sts-radio .value=${TransformType.Scale}>
<sts-icon icon="scale">Scale</sts-icon>
</sts-radio>
</sts-radio-group>
</sts-toolbar-group>
<hr />
<sts-toolbar-group>
<sts-button class="stylize" primary>
Stylize <sts-icon icon="chevron-right"></sts-icon>
</sts-button>
</sts-toolbar-group>
</sts-toolbar>
<div class="abs h-end v-start offset">
<sts-button>Switch to Camera View</sts-button>
</div>
<sts-anim-timeline-controls
class="timeline-controls abs h-center v-end"
></sts-anim-timeline-controls>
${this.#selectedObject != null ? html`
<div class="inspector abs h-end v-end offset">
<header class="inspector__header">
<div class="object-name">
${this.#selectedObject.icon != null ? html`
<sts-icon .icon=${this.#selectedObject.icon}></sts-icon>
` : nothing}
<h3>${this.#selectedObject.name}</h3>
</div>
<sts-button
class="swap-object"
icon="swap"
>
Swap object
</sts-button>
</header>
<sts-entity-inspector></sts-entity-inspector>
<footer class="inspector__footer">
${this.#selectedEntityInTracklist ? html`
<sts-button
class="add-keyframe"
keybind="K"
@click=${() => this._timeline.addKeyframe()}
>
Add Keyframe
</sts-button>
` : this._sceneData.selectedEntity != null ? html`
<sts-button
class="add-to-timeline"
@click=${() => this._timeline.addTrack(this._sceneData.selectedEntity!)}
>
Add to Timeline
</sts-button>
` : nothing}
<sts-button
class="delete"
icon="trash"
title="Delete Entity"
></sts-button>
</footer>
</div>
` : nothing}
</sts-studio>
<sts-menu
autoClose
.trigger=${this.#fileTriggerRef}
>
<sts-button
role="menuitem"
keybind="Ctrl+N"
@click=${() => {
// TODO
}}
>
New scene
</sts-button>
<sts-button
role="menuitem"
keybind="Ctrl+Shift+N"
@click=${() => {
// TODO
}}
>
New scene from template...
</sts-button>
<hr />
<sts-button
role="menuitem"
keybind="Ctrl+S"
@click=${() => {
// TODO: Should we show a spinner here while we're waiting?
// Toast message for success/failure?
// TODO: Need to disable this when the scene is not ours
studio.saveScene();
}}
>
Save scene
</sts-button>
<sts-button
role="menuitem"
keybind="Ctrl+Shift+S"
@click=${() => {
// TODO
}}
>
Save scene as copy...
</sts-button>
</sts-menu>
<sts-dialog
.trigger=${this.#mediaLibraryTriggerRef}
tag="sts-media-library-dialog"
nonBlocking
></sts-dialog>
`;
}
}
+20 -1
View File
@@ -10,14 +10,33 @@ html, body {
--color-panel-background: #1e1e2c;
--color-toolbar-background: #2c2c3b;
--color-toolbar-background-light: #595970;
--color-toolbar-background-light: #595970;
--color-panel-item-name-background: #39394d;
--color-panel-item-background: #55556b;
--color-accent: #fc6b68;
--color-accent-hover: #FF8D8B;
--color-accent-fade: #fc6b6840;
--color-text: #dbdbdf;
--color-text-bright: #ffffff;
--color-text-disabled: #7c7c85;
--axis-red: rgb(220, 53, 69);
--axis-green: rgb(25, 135, 84);
--axis-blue: rgb(13, 110, 253);
--corner-radius: 0.75rem;
--padding: 0.4rem;
--neutral-100: #242433;
--neutral-200: #39394D;
--neutral-250: #44445C;
--neutral-300: #515168;
--neutral-350: #505068;
--neutral-400: #676781;
--neutral-450: #6D6D80;
}
body {
color-scheme: dark;
font-family: "Fira Sans", sans-serif;
}
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill-rule="evenodd" clip-rule="evenodd" d="M234.474 5.70813C248.394 0.736778 263.606 0.736772 277.526 5.70813L469.526 74.2796C494.997 83.3765 512 107.504 512 134.551V376.996C512 403.997 495.055 428.093 469.645 437.225L277.645 506.225C263.653 511.253 248.347 511.253 234.355 506.225L42.3553 437.225C16.9454 428.093 0 403.997 0 376.996V134.551C0 107.504 17.0029 83.3765 42.4745 74.2796L234.474 5.70813ZM429.489 127.94L256 65.9796L82.4227 127.971L255.524 189.544L429.489 127.94ZM448 189.279L288 245.938V434.496L448 376.996V189.279ZM224 434.496V246.259L64 189.347V376.996L224 434.496ZM245.318 193.159C245.325 193.156 245.332 193.154 245.339 193.151L245.318 193.159C245.318 193.159 245.318 193.159 245.318 193.159Z"/>
</svg>

After

Width:  |  Height:  |  Size: 789 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill-rule="evenodd" clip-rule="evenodd" d="M472.659 117C468.967 76.737 435.884 47.8502 401.126 30.471C362.331 11.0738 310.977 0 256 0C201.023 0 149.669 11.0738 110.874 30.471C76.1159 47.8502 43.0325 76.737 39.3411 117H39V394.5H39.3885C43.3068 434.502 76.2599 463.222 110.874 480.529C149.669 499.926 201.023 511 256 511C310.977 511 362.331 499.926 401.126 480.529C435.74 463.222 468.693 434.502 472.611 394.5H473V117H472.659ZM103 124.5C103 117.607 109.419 102.753 139.496 87.7143C167.659 73.633 208.804 64 256 64C303.196 64 344.341 73.633 372.504 87.7143C402.581 102.753 409 117.607 409 124.5C409 131.393 402.581 146.247 372.504 161.286C344.341 175.367 303.196 185 256 185C208.804 185 167.659 175.367 139.496 161.286C109.419 146.247 103 131.393 103 124.5ZM409 214.372C406.393 215.824 403.764 217.21 401.126 218.529C362.331 237.926 310.977 249 256 249C201.023 249 149.669 237.926 110.874 218.529C108.236 217.21 105.607 215.824 103 214.372V386.5C103 393.393 109.419 408.247 139.496 423.286C167.659 437.367 208.804 447 256 447C303.196 447 344.341 437.367 372.504 423.286C402.581 408.247 409 393.393 409 386.5V214.372Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill-rule="evenodd" clip-rule="evenodd" d="M512 256C512 397.385 397.385 512 256 512C114.615 512 0 397.385 0 256C0 114.615 114.615 0 256 0C397.385 0 512 114.615 512 256ZM433.391 329.592C404.517 399.111 335.968 448 256 448C176.032 448 107.483 399.111 78.6092 329.592C81.9175 330.672 85.2815 331.705 88.6902 332.694C132.861 345.512 192.004 353 256 353C319.996 353 379.139 345.512 423.31 332.694C426.719 331.705 430.083 330.672 433.391 329.592ZM447.961 252.089C447.186 252.787 446.015 253.733 444.294 254.914C436.913 259.977 424.138 265.814 405.474 271.23C368.573 281.938 315.716 289 256 289C196.284 289 143.427 281.938 106.526 271.23C87.8618 265.814 75.0866 259.977 67.7061 254.914C65.9853 253.733 64.8139 252.787 64.039 252.089C66.122 147.855 151.268 64 256 64C360.732 64 445.878 147.855 447.961 252.089Z"/>
</svg>

After

Width:  |  Height:  |  Size: 882 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill-rule="evenodd" clip-rule="evenodd" d="M256 312C280.482 312 305.414 307.158 326.299 296.715C343.834 287.948 376 265.475 376 224C376 182.525 343.834 160.052 326.299 151.285C305.414 140.842 280.482 136 256 136C231.518 136 206.586 140.842 185.701 151.285C168.166 160.052 136 182.525 136 224C136 265.475 168.166 287.948 185.701 296.715C206.586 307.158 231.518 312 256 312ZM320 224C320 241.673 291.346 256 256 256C220.654 256 192 241.673 192 224C192 206.327 220.654 192 256 192C291.346 192 320 206.327 320 224Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M512 306C512 386.5 406 464 256 464C106 464 0 386.5 0 306V206C0 125.5 106 47 256 47C406 47 512 125.5 512 206V306ZM456 306C456 320.522 445.756 344.798 409.712 368.247C374.831 390.939 321.795 408 256 408C190.205 408 137.169 390.939 102.288 368.247C66.2443 344.798 56 320.522 56 306V206C56 191.365 66.3738 166.816 102.469 143.135C137.367 120.239 190.35 103 256 103C321.65 103 374.633 120.239 409.531 143.135C445.626 166.816 456 191.365 456 206V306Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M32 95.9974C40.4869 95.9974 48.6263 92.6261 54.6274 86.6252C60.6286 80.6243 64 72.4853 64 63.9987C64 55.5121 60.6286 47.3731 54.6274 41.3722C48.6263 35.3713 40.4869 32 32 32C23.5131 32 15.3737 35.3713 9.37258 41.3722C3.37142 47.3731 0 55.5121 0 63.9987C0 72.4853 3.37142 80.6243 9.37258 86.6252C15.3737 92.6261 23.5131 95.9974 32 95.9974ZM288 63.9987C288 46.2994 273.7 32 256 32C238.3 32 224 46.2994 224 63.9987V272.99L43.5 423.384C29.9 434.684 28.1 454.883 39.4 468.482C50.7 482.082 70.9 483.882 84.5 472.582L256 329.688L427.5 472.582C441.1 483.882 461.3 482.082 472.6 468.482C483.9 454.883 482.1 434.684 468.5 423.384L288 272.99V63.9987ZM384 95.9974C392.487 95.9974 400.626 92.6261 406.627 86.6252C412.629 80.6243 416 72.4853 416 63.9987C416 55.5121 412.629 47.3731 406.627 41.3722C400.626 35.3713 392.487 32 384 32C375.513 32 367.374 35.3713 361.373 41.3722C355.371 47.3731 352 55.5121 352 63.9987C352 72.4853 355.371 80.6243 361.373 86.6252C367.374 92.6261 375.513 95.9974 384 95.9974ZM416 255.991C416 247.504 412.629 239.365 406.627 233.364C400.626 227.363 392.487 223.992 384 223.992C375.513 223.992 367.374 227.363 361.373 233.364C355.371 239.365 352 247.504 352 255.991C352 264.477 355.371 272.616 361.373 278.617C367.374 284.618 375.513 287.99 384 287.99C392.487 287.99 400.626 284.618 406.627 278.617C412.629 272.616 416 264.477 416 255.991ZM480 191.993C488.487 191.993 496.626 188.622 502.627 182.621C508.629 176.62 512 168.481 512 159.995C512 151.508 508.629 143.369 502.627 137.368C496.626 131.367 488.487 127.996 480 127.996C471.513 127.996 463.374 131.367 457.373 137.368C451.371 143.369 448 151.508 448 159.995C448 168.481 451.371 176.62 457.373 182.621C463.374 188.622 471.513 191.993 480 191.993ZM160 159.995C160 151.508 156.629 143.369 150.627 137.368C144.626 131.367 136.487 127.996 128 127.996C119.513 127.996 111.374 131.367 105.373 137.368C99.3714 143.369 96 151.508 96 159.995C96 168.481 99.3714 176.62 105.373 182.621C111.374 188.622 119.513 191.993 128 191.993C136.487 191.993 144.626 188.622 150.627 182.621C156.629 176.62 160 168.481 160 159.995ZM64 255.991C64 247.504 60.6286 239.365 54.6274 233.364C48.6263 227.363 40.4869 223.992 32 223.992C23.5131 223.992 15.3737 227.363 9.37258 233.364C3.37142 239.365 0 247.504 0 255.991C0 264.477 3.37142 272.616 9.37258 278.617C15.3737 284.618 23.5131 287.99 32 287.99C40.4869 287.99 48.6263 284.618 54.6274 278.617C60.6286 272.616 64 264.477 64 255.991ZM256 479.982C264.487 479.982 272.626 476.61 278.627 470.609C284.629 464.609 288 456.47 288 447.983C288 439.496 284.629 431.357 278.627 425.357C272.626 419.356 264.487 415.984 256 415.984C247.513 415.984 239.374 419.356 233.373 425.357C227.371 431.357 224 439.496 224 447.983C224 456.47 227.371 464.609 233.373 470.609C239.374 476.61 247.513 479.982 256 479.982Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+36 -9
View File
@@ -1,13 +1,15 @@
@use "sass:math";
:host {
display: inline-flex;
align-items: center;
align-items: baseline;
gap: 0.75rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font: {
family: "Fira Sans", sans-serif;
size: 15px;
size: math.div(15, 16) * 1rem;
weight: 500;
}
line-height: 1.5;
@@ -16,9 +18,8 @@
cursor: pointer;
user-select: none;
background: var(--background-color, #0006);
color: #FFFC;
backdrop-filter: blur(16px);
background: var(--neutral-200);
color: #FFF;
transition: {
property: background, color;
duration: 0.1s;
@@ -26,11 +27,20 @@
}
}
:host(:hover) {
background: #0008;
color: #FFFF;
:host(.icon-only) {
padding-inline: 0.5rem;
}
:host(:hover) {
background: var(--neutral-250);
}
:host(.primary) { background: var(--color-accent) }
:host(.primary:hover) { background: var(--color-accent-hover) }
:host(.secondary) { background: var(--neutral-400) }
:host(.secondary:hover) { background: var(--neutral-450) }
:host(.disabled),
:host(.disabled:hover) {
background: #0003;
@@ -38,6 +48,23 @@
cursor: default;
}
.icon {
.icon, .dropdown-icon {
align-self: center;
fill: currentColor;
}
.dropdown-icon {
font-size: 0.75rem;
}
.keybind {
// margin-inline-start: auto;
flex: 1 0 0;
margin-inline-start: 2.25rem;
color: rgba(#FFF, 0.75);
font: {
size: 0.875rem;
weight: 400;
}
text-align: end;
}
+51 -11
View File
@@ -1,6 +1,7 @@
import { on } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import styles from "./button.element.scss?inline";
@@ -16,11 +17,33 @@ export class ButtonElement extends LitElement {
@property() icon?: string;
@property({ type: Boolean }) disabled = false;
@property({ type: Boolean })
primary?: boolean;
@property({ type: Boolean }) primary?: boolean;
@property({ type: Boolean }) secondary?: boolean;
@property({ type: Boolean }) dropdown?: boolean;
@property() keybind?: string;
@property({ reflect: true, type: Number })
override tabIndex = 0;
@state() _hasSlottedContent = false;
#defaultSlotRef = createRef<HTMLSlotElement>();
_checkForSlotContent(): void {
const nodes = this.#defaultSlotRef.value?.assignedNodes() ?? [];
this._hasSlottedContent = (
nodes.length > 0
&& nodes.some(node => Boolean(node.textContent?.trim()))
);
}
override connectedCallback(): void {
super.connectedCallback();
this._checkForSlotContent();
}
protected override update(changes: PropertyValues<this>): void {
if (changes.has("disabled")) {
this.ariaDisabled = this.disabled ? "true" : null;
@@ -28,13 +51,14 @@ export class ButtonElement extends LitElement {
this.classList.toggle("disabled", this.disabled);
}
if (changes.has("primary")) {
if (this.primary) {
this.style.setProperty("--background-color", "var(--color-accent)");
} else {
this.style.removeProperty("--background-color");
}
}
if (changes.has("primary"))
this.classList.toggle("primary", Boolean(this.primary));
if (changes.has("secondary"))
this.classList.toggle("secondary", Boolean(this.secondary))
if (changes.has("_hasSlottedContent"))
this.classList.toggle("icon-only", !this._hasSlottedContent);
super.update(changes);
}
@@ -50,11 +74,27 @@ export class ButtonElement extends LitElement {
protected override render = () => html`
${this.icon ? html`
<sts-icon
class="icon"
part="icon"
icon=${this.icon}
aria-hidden="true"
></sts-icon>
` : nothing}
<slot></slot>
<slot
${ref(this.#defaultSlotRef)}
@slotchange=${this._checkForSlotContent}
></slot>
${this.keybind ? html`
<span class="keybind">${this.keybind}</span>
` : nothing}
${this.dropdown ? html`
<sts-icon
class="dropdown-icon"
part="dropdown-icon"
icon="chevron-down"
></sts-icon>
` : nothing}
`
}
+7 -1
View File
@@ -1,6 +1,6 @@
import { deref, inject } from "@storyteller/framework";
import { type ArrayOrSingle, asArray, exists } from "@storyteller/utility";
import { LitElement, TemplateResult, type PropertyValues } from "lit";
import { LitElement, TemplateResult, type PropertyValues, css } from "lit";
import { html, unsafeStatic } from "lit/static-html.js";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -86,6 +86,12 @@ import { type DialogBaseElement } from "./dialog-base.element";
*/
@customElement("sts-dialog")
export class DialogElement extends LitElement {
static override styles = css`
:host {
display: none;
}
`;
/**
* Tag-name of a custom element inheriting from {@linkcode DialogBaseElement}`
*/
+2 -1
View File
@@ -1,3 +1,4 @@
export * from "./form-field-base.element";
export * from "./text-field.element";
export * from "./scalar-field.element";
export * from "./select";
export * from "./text-field.element";
+5
View File
@@ -0,0 +1,5 @@
import "./radio-group.element";
import "./radio.element";
export * from "./radio-group.element";
export * from "./radio.element";
@@ -0,0 +1,4 @@
:host {
display: flex;
gap: 0.25rem;
}
@@ -0,0 +1,61 @@
import { on } from "@storyteller/framework";
import { PropertyValues, html, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { FormFieldBaseElement } from "../form-field-base.element";
import { RadioElement } from "./radio.element";
import styles from "./radio-group.element.scss?inline";
@customElement("sts-radio-group")
export class RadioGroupElement extends FormFieldBaseElement {
static override styles = unsafeCSS(styles);
@property({ reflect: true })
override role = "radiogroup";
@property({ attribute: false })
value?: any;
#slotRef = createRef<HTMLSlotElement>();
override connectedCallback(): void {
super.connectedCallback();
this.#checkValue();
}
protected override update(changes: PropertyValues<this>): void {
if (changes.has("value"))
this.#checkValue();
super.update(changes);
}
@on("click", { capture: true })
onClick(event: PointerEvent): void {
if (event.target instanceof RadioElement) {
this.dispatchEvent(new CustomEvent("value-change", {
detail: event.target.value,
}));
}
}
#checkValue(): void {
for (let node of this.#slotRef.value?.assignedElements() ?? []) {
if (node instanceof RadioElement) {
node.checked = node.value != null && node.value === this.value;
if (node.checked)
this.internals.ariaValueText = node.textContent;
}
}
}
protected override render = () => html`
<slot
${ref(this.#slotRef)}
@slotchange=${this.#checkValue}
></slot>
`;
}
@@ -0,0 +1,30 @@
:host {
position: relative;
padding: 0.5rem;
border-radius: 0.5rem;
color: #FFF;
outline: transparent 2px solid;
transition: {
property: background, outline-color;
duration: 100ms;
timing-function: linear;
}
cursor: pointer;
}
::slotted(*) {
pointer-events: none;
}
:host(:hover) {
background: var(--neutral-250);
}
:host(.checked) {
outline-color: var(--color-accent);
}
:host(.checked:hover) {
background: var(--color-accent-fade);
}
@@ -0,0 +1,29 @@
import { LitElement, PropertyValues, html, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import styles from "./radio.element.scss?inline";
@customElement("sts-radio")
export class RadioElement extends LitElement {
static override styles = unsafeCSS(styles);
@property({ reflect: true })
override role = "radio";
@property({ attribute: false })
value?: any;
@property({ type: Boolean })
checked = false;
protected override update(changes: PropertyValues<this>): void {
if (changes.has("checked"))
this.classList.toggle("checked", this.checked);
super.update(changes);
}
protected override render = () => html`
<slot></slot>
`;
}
@@ -4,28 +4,33 @@
box-sizing: border-box;
display: block;
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
border-radius: 0.5rem;
background: var(--neutral-200);
border: 1px solid transparent;
color: #FFF;
outline: transparent 2px solid {
offset: -2px;
}
background: #0004;
border: 1px solid #FFF2;
color: #FFFC;
outline: none !important;
transition: {
property: background-color, border-color, color;
duration: 33.333ms;
property: background-color, outline-color;
duration: 100ms;
timing-function: linear;
}
font: {
family: "Fira Sans", sans-serif;
size: math.div(15, 16) * 1rem;
size: math.div(14, 16) * 1rem;
weight: 500;
}
line-height: 1;
white-space: nowrap;
}
:host(:focus-within) {
border-color: #FFF4;
background: #0006;
color: #FFFF;
outline-color: var(--color-accent);
background: var(--neutral-100)
}
:host(:not(.editing)) {
@@ -45,4 +50,8 @@
&:first-line {
display: inline-block;
}
&::selection {
background: var(--neutral-300);
}
}
+5
View File
@@ -0,0 +1,5 @@
import "./select.element";
import "./option.element";
export * from "./select.element";
export * from "./option.element";
@@ -0,0 +1,7 @@
:host {
padding: 0.375rem 0;
border-radius: 0.5rem;
background: var(--neutral-350);
transition: background 100ms linear;
overflow: hidden;
}
@@ -0,0 +1,16 @@
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import styles from "./listbox.element.scss?inline";
@customElement("sts-listbox")
export class ListboxElement extends LitElement {
static override styles = unsafeCSS(styles);
@property({ reflect: true })
override role = "listbox";
protected override render = () => html`
<slot></slot>
`;
}
@@ -0,0 +1,28 @@
@use "sass:math";
:host {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
color: #FFF;
transition: background 100ms linear;
cursor: pointer;
user-select: none;
font-size: math.div(15, 16) * 1rem;
line-height: 1;
}
:host(:hover) {
background: rgba(#FFF, 0.1);
}
:host(.selected) {
background: var(--neutral-200);
}
::slotted(*) {
pointer-events: none;
}
@@ -0,0 +1,35 @@
import { LitElement, PropertyValues, html, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import styles from "./option.element.scss?inline";
@customElement("sts-option")
export class OptionElement extends LitElement {
static override styles = unsafeCSS(styles);
@property({ reflect: true })
override role = "option";
@property({ attribute: false })
value?: any;
@property({ type: Boolean })
selected?: boolean;
protected override update(changes: PropertyValues<this>): void {
if (changes.has("selected")) {
this.classList.toggle("selected", this.selected);
if (this.selected)
this.setAttribute("aria-selected", "true")
else
this.removeAttribute("aria-selected");
}
super.update(changes);
}
protected override render = () => html`
<slot></slot>
`;
}
@@ -0,0 +1,40 @@
@use "sass:math";
:host {
padding: 0.5rem;
background: var(--neutral-350);
color: #FFF;
transition: {
property: background;
duration: 100ms;
timing-function: linear;
}
user-select: none;
cursor: pointer;
font-size: math.div(15, 16) * 1rem;
line-height: 1;
}
:host(:hover) {
background: var(--neutral-400);
}
.selected {
display: flex;
align-items: center;
gap: 0.75rem;
sts-option {
padding: 0;
}
}
.dropdown-icon {
font-size: 0.75rem;
}
.hidden {
display: none;
}
@@ -0,0 +1,148 @@
import { inject, on } from "@storyteller/framework";
import { PropertyValues, html, nothing, unsafeCSS } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { styleMap } from "lit/directives/style-map.js";
import {
OVERLAY_PROVIDER,
OverlayHandle,
type OverlayProvider,
} from "../../overlay-provider.element";
import { FormFieldBaseElement } from "../form-field-base.element";
import { OptionElement } from "./option.element";
import "./listbox.element";
import styles from "./select.element.scss?inline";
@customElement("sts-select")
export class SelectElement extends FormFieldBaseElement {
static override styles = unsafeCSS(styles);
@property({ reflect: true })
override role = "combobox";
@property({ attribute: false })
value?: any;
@property()
placeholder?: string;
@property({ type: Number, reflect: true })
override tabIndex = 0;
@inject(OVERLAY_PROVIDER)
_overlay!: OverlayProvider
@state() _selected?: OptionElement;
#slotRef = createRef<HTMLSlotElement>();
#overlayHandle?: WeakRef<OverlayHandle>;
get #selectedOption() {
return this.#slotRef.value
?.assignedNodes()
.find((node: Node): node is OptionElement => (
node.nodeType === Node.ELEMENT_NODE
&& node instanceof OptionElement
&& node.value === this.value
));
}
override connectedCallback(): void {
super.connectedCallback();
this.#checkValue();
}
protected override update(changes: PropertyValues<this>): void {
if (changes.has("value"))
this.#checkValue();
super.update(changes);
}
@on("click")
onClick(): void {
const nodes = this.#slotRef.value
?.assignedNodes()
.map(node => {
const cloned = node.cloneNode(true);
if (
cloned.nodeType === Node.ELEMENT_NODE
&& cloned instanceof OptionElement
) {
cloned.value = (node as OptionElement).value;
cloned.selected = cloned.value === this.value;
}
return cloned;
});
const rect = this.getBoundingClientRect();
this.#overlayHandle = this._overlay.attach(this, html`
<sts-listbox
style=${styleMap({
position: "absolute",
top: `${rect.bottom + 4}px`,
left: `${rect.left}px`,
minWidth: `${rect.width}px`,
})}
@click=${this.onListboxClick}
>
${nodes ?? nothing}
</sts-listbox>
`, {
onBackdropClick: () => this.#close(),
});
}
onListboxClick(event: PointerEvent): void {
if (event.target instanceof OptionElement) {
this.dispatchEvent(new CustomEvent("value-change", {
detail: event.target.value,
}));
this.#close();
}
}
#checkValue(): void {
const cloned = this.#selectedOption?.cloneNode(true) as OptionElement | undefined;
if (cloned) {
this.internals.ariaValueText = cloned.textContent;
cloned.removeAttribute("role");
cloned.style.setProperty("pointer-events", "none");
this._selected = cloned;
}
}
#close(): void {
if (!this.#overlayHandle) return;
this._overlay.detach(this.#overlayHandle);
this.#overlayHandle = undefined;
}
protected override render = () => html`
<div class="selected">
${this._selected ?? html`
<div class="placeholder">
${this.placeholder ?? nothing}
</div>
`}
<sts-icon
class="dropdown-icon"
icon="chevron-down"
></sts-icon>
</div>
<div class="hidden">
<slot
${ref(this.#slotRef)}
@slotchange=${this.#checkValue}
></slot>
</div>
`;
}
+45 -35
View File
@@ -4,13 +4,12 @@ import { LitElement, TemplateResult, html, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
import farCrosshairs from "@fortawesome/fontawesome-pro/svgs/regular/crosshairs.svg?raw";
import farVideo from "@fortawesome/fontawesome-pro/svgs/regular/video.svg?raw";
import fasCrosshairs from "@fortawesome/fontawesome-pro/svgs/solid/crosshairs.svg?raw";
import fasVideo from "@fortawesome/fontawesome-pro/svgs/solid/video.svg?raw";
import fasCircleQuestion from "@fortawesome/fontawesome-pro/svgs/solid/circle-question.svg?raw";
import fasKey from "@fortawesome/fontawesome-pro/svgs/solid/key.svg?raw";
import farCube from "@fortawesome/fontawesome-pro/svgs/regular/cube.svg?raw";
import farGlobe from "@fortawesome/fontawesome-pro/svgs/regular/globe.svg?raw";
import farUpDownLeftRight from "@fortawesome/fontawesome-pro/svgs/regular/arrows-up-down-left-right.svg?raw";
import fasGlobe from "@fortawesome/fontawesome-pro/svgs/solid/globe.svg?raw";
import fasUpDownLeftRight from "@fortawesome/fontawesome-pro/svgs/solid/arrows-up-down-left-right.svg?raw";
import fasPlay from "@fortawesome/fontawesome-pro/svgs/solid/play.svg?raw";
import fasPause from "@fortawesome/fontawesome-pro/svgs/solid/pause.svg?raw";
import fasFwdStep from "@fortawesome/fontawesome-pro/svgs/solid/forward-step.svg?raw";
@@ -18,12 +17,12 @@ import fasFwdFast from "@fortawesome/fontawesome-pro/svgs/solid/forward-fast.svg
import fasBwdStep from "@fortawesome/fontawesome-pro/svgs/solid/backward-step.svg?raw";
import fasBwdFast from "@fortawesome/fontawesome-pro/svgs/solid/backward-fast.svg?raw";
import fasQuestion from "@fortawesome/fontawesome-pro/svgs/solid/question.svg?raw";
import farRotate from "@fortawesome/fontawesome-pro/svgs/regular/arrows-rotate.svg?raw";
import farScale from "@fortawesome/fontawesome-pro/svgs/regular/arrow-up-right-and-arrow-down-left-from-center.svg?raw";
import fasRotate from "@fortawesome/fontawesome-pro/svgs/solid/arrows-rotate.svg?raw";
import fasScale from "@fortawesome/fontawesome-pro/svgs/solid/arrow-up-right-and-arrow-down-left-from-center.svg?raw";
import fasCaretRight from "@fortawesome/fontawesome-pro/svgs/solid/caret-right.svg?raw";
import fasFloppyDisk from "@fortawesome/fontawesome-pro/svgs/solid/floppy-disk.svg?raw";
import farSkeleton from "@fortawesome/fontawesome-pro/svgs/regular/skeleton.svg?raw";
import farBone from "@fortawesome/fontawesome-pro/svgs/regular/bone.svg?raw";
import fasSkeleton from "@fortawesome/fontawesome-pro/svgs/regular/skeleton.svg?raw";
import fasBone from "@fortawesome/fontawesome-pro/svgs/regular/bone.svg?raw";
import fasBrightness from "@fortawesome/fontawesome-pro/svgs/solid/brightness.svg?raw";
import fasCheck from "@fortawesome/fontawesome-pro/svgs/solid/check.svg?raw";
import fasXmark from "@fortawesome/fontawesome-pro/svgs/solid/xmark.svg?raw";
@@ -33,23 +32,29 @@ import fasMinus from "@fortawesome/fontawesome-pro/svgs/solid/minus.svg?raw";
import fasListTree from "@fortawesome/fontawesome-pro/svgs/solid/list-tree.svg?raw";
import fasBars from "@fortawesome/fontawesome-pro/svgs/solid/bars.svg?raw";
import fasPlusLarge from "@fortawesome/fontawesome-pro/svgs/solid/plus-large.svg?raw";
import farFileImport from "@fortawesome/fontawesome-pro/svgs/regular/file-import.svg?raw";
import farPhotoFilmMusic from "@fortawesome/fontawesome-pro/svgs/regular/photo-film-music.svg?raw";
import fasFileImport from "@fortawesome/fontawesome-pro/svgs/regular/file-import.svg?raw";
import fasPhotoFilmMusic from "@fortawesome/fontawesome-pro/svgs/regular/photo-film-music.svg?raw";
import fasChevronDown from "@fortawesome/fontawesome-pro/svgs/solid/chevron-down.svg?raw";
import farVolume from "@fortawesome/fontawesome-pro/svgs/regular/volume.svg?raw";
import farFilm from "@fortawesome/fontawesome-pro/svgs/regular/film.svg?raw";
import fasVolume from "@fortawesome/fontawesome-pro/svgs/regular/volume.svg?raw";
import fasFilm from "@fortawesome/fontawesome-pro/svgs/regular/film.svg?raw";
import fasPersonRunning from "@fortawesome/fontawesome-pro/svgs/solid/person-running.svg?raw";
import farMountainSun from "@fortawesome/fontawesome-pro/svgs/regular/mountain-sun.svg?raw";
import farArrowRightToBracket from "@fortawesome/fontawesome-pro/svgs/regular/arrow-right-to-bracket.svg?raw";
import farMagnifyingGlass from "@fortawesome/fontawesome-pro/svgs/regular/magnifying-glass.svg?raw";
import fasMountainSun from "@fortawesome/fontawesome-pro/svgs/regular/mountain-sun.svg?raw";
import fasArrowRightToBracket from "@fortawesome/fontawesome-pro/svgs/regular/arrow-right-to-bracket.svg?raw";
import fasMagnifyingGlass from "@fortawesome/fontawesome-pro/svgs/regular/magnifying-glass.svg?raw";
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 fasFile from "@fortawesome/fontawesome-pro/svgs/solid/file.svg?raw";
import fasTrash from "@fortawesome/fontawesome-pro/svgs/solid/trash.svg?raw";
import fasRightLeft from "@fortawesome/fontawesome-pro/svgs/solid/right-left.svg?raw";
import fasChevronRight from "@fortawesome/fontawesome-pro/svgs/solid/chevron-right.svg?raw";
import customCylinder from "../assets/icons/cylinder.svg?raw";
import customSphere from "../assets/icons/sphere.svg?raw";
import customTorus from "../assets/icons/torus.svg?raw";
import customCube from "../assets/icons/cube-solid.svg?raw";
import customCylinder from "../assets/icons/cylinder-solid.svg?raw";
import customSphere from "../assets/icons/sphere-solid.svg?raw";
import customTorus from "../assets/icons/torus-solid.svg?raw";
import customXformLocal from "../assets/icons/xform-local.svg?raw";
import styles from "./icon.element.scss?inline";
@@ -77,49 +82,54 @@ function renderSvg(iconId: string) {
const svgSrc = match (iconId, {
"bars": () => fasBars,
"bone": () => farBone,
"bone": () => fasBone,
"bwd-fast": () => fasBwdFast,
"bwd-step": () => fasBwdStep,
"caret-right": () => fasCaretRight,
"check": () => fasCheck,
"chevron-down": () => fasChevronDown,
"chevron-right": () => fasChevronRight,
"circle-check": () => fasCircleCheck,
"circle-question": () => fasCircleQuestion,
"circle-xmark": () => fasCircleXmark,
"crosshairs": () => farCrosshairs,
"cube": () => farCube,
"crosshairs": () => fasCrosshairs,
"cube": () => customCube,
"cylinder": () => customCylinder,
"figure": () => fasPersonRunning,
"file-import": () => farFileImport,
"film": () => farFilm,
"file": () => fasFile,
"file-import": () => fasFileImport,
"film": () => fasFilm,
"films": () => fasFilms,
"floppy-disk": () => fasFloppyDisk,
"fwd-fast": () => fasFwdFast,
"fwd-step": () => fasFwdStep,
"home": () => fasHouse,
"import": () => farArrowRightToBracket,
"import": () => fasArrowRightToBracket,
"key": () => fasKey,
"light": () => fasBrightness,
"list-tree": () => fasListTree,
"minus": () => fasMinus,
"globe": () => farGlobe,
"photo-film-music": () => farPhotoFilmMusic,
"globe": () => fasGlobe,
"photo-film-music": () => fasPhotoFilmMusic,
"play": () => fasPlay,
"pause": () => fasPause,
"plus-large": () => fasPlusLarge,
"question": () => fasQuestion,
"record": () => fasRecord,
"rotate": () => farRotate,
"scale": () => farScale,
"scene": () => farMountainSun,
"search": () => farMagnifyingGlass,
"skeleton": () => farSkeleton,
"rotate": () => fasRotate,
"scale": () => fasScale,
"scene": () => fasMountainSun,
"search": () => fasMagnifyingGlass,
"skeleton": () => fasSkeleton,
"sphere": () => customSphere,
"swap": () => fasRightLeft,
"trash": () => fasTrash,
"torus": () => customTorus,
"up-down-left-right": () => farUpDownLeftRight,
"up-down-left-right": () => fasUpDownLeftRight,
"upload": () => fasUpload,
"video": () => farVideo,
"volume": () => farVolume,
"video": () => fasVideo,
"volume": () => fasVolume,
"xform-local": () => customXformLocal,
"xmark": () => fasXmark,
_: () => null,
});
+3 -10
View File
@@ -5,24 +5,17 @@
padding: 0.5rem 0;
min-width: 220px;
background: #0006;
backdrop-filter: blur(16px);
background: var(--neutral-200);
overflow: hidden;
}
::slotted(sts-button) {
background: unset;
backdrop-filter: unset;
border-radius: unset;
}
::slotted(sts-button:hover) {
background: #0004;
// FIXME: https://github.com/w3c/csswg-drafts/issues/3896
&::part(icon) {
opacity: 0.75;
}
background: var(--neutral-300);
}
::slotted(hr) {
@@ -31,5 +24,5 @@
border: none;
height: 1px;
margin: 0.5rem 0;
background: #FFF1;
background: rgba(#FFF, 0.1);
}
+1 -1
View File
@@ -60,7 +60,7 @@ import panelStyles from "./menu.element.scss?inline";
export class MenuElement extends LitElement {
static override styles = css`
:host {
visibility: hidden;
display: none;
}
`;
+2 -16
View File
@@ -1,22 +1,8 @@
:host {
display: flex;
gap: 2px;
gap: 0.25rem;
}
::slotted(*) {
border-radius: var(--corner-radius);
}
::slotted(:first-child) {
border: {
top-left-radius: 8px;
bottom-left-radius: 8px;
}
}
::slotted(:last-child) {
border: {
top-right-radius: 8px;
bottom-right-radius: 8px;
}
border-radius: 0.5rem;
}
+13 -1
View File
@@ -1,4 +1,16 @@
:host {
display: flex;
gap: 16px;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 0.5rem;
background: var(--neutral-200);
}
::slotted(hr) {
display: block;
width: 2px;
height: 1.125rem;
border: none;
background: rgba(#FFF, 0.1);
}
+4 -18
View File
@@ -1,5 +1,5 @@
import { type DynamicProvider, inject, UniqueToken } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, nothing, unsafeCSS } from "lit";
import { type DynamicProvider, inject, UniqueToken, observe } from "@storyteller/framework";
import { LitElement, html, nothing, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -34,23 +34,9 @@ export class TreeElement extends LitElement {
return !!this.id && this._selectionProvider?.selectedId === this.id
}
@observe(["selectedId"])
@inject(TREE_SELECTION_PROVIDER)
_selectionProvider?: TreeSelectionProvider;
override connectedCallback(): void {
this._selectionProvider?.addEventListener("property-changes", this.onProviderChanges);
super.connectedCallback();
}
override disconnectedCallback(): void {
this._selectionProvider?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<TreeSelectionProvider>>) => {
if (changes.has("selectedId"))
this.requestUpdate();
}
_selectionProvider!: TreeSelectionProvider;
toggleExpanded(): void {
this.expanded = !this.expanded;
@@ -3,12 +3,9 @@
display: flex;
flex-direction: column;
gap: var(--padding);
gap: 1rem;
overflow: auto;
--background-color: var(--color-panel-background);
color: #FFFC;
color: #FFF;
&::-webkit-scrollbar {
width: 0.5rem;
@@ -32,13 +29,3 @@
*, *::before, *::after {
box-sizing: border-box;
}
.container {
background-color: var(--background-color);
border-radius: var(--corner-radius);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: var(--padding);
gap: var(--padding);
}
@@ -1,5 +1,5 @@
import { inject } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, unsafeCSS, nothing } from "lit";
import { inject, observe } from "@storyteller/framework";
import { LitElement, html, unsafeCSS, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { SCENE_DATA_PROVIDER, type SceneDataProvider } from "../scene-data-provider.element";
@@ -16,89 +16,69 @@ import styles from "./entity-inspector.element.scss?inline";
export class EntityInspectorElement extends LitElement {
static override styles = unsafeCSS(styles);
@observe(["selectionComponents"])
@inject(SCENE_DATA_PROVIDER)
_sceneData?: SceneDataProvider;
_sceneData!: SceneDataProvider;
override connectedCallback(): void {
this._sceneData?.addEventListener("property-changes", this.onProviderChanges);
super.connectedCallback();
}
protected override render = () => html`
${this._sceneData
?.selectionComponents
.slice()
.sort((a, b) => {
const ordA = COMPONENT_SORT_ORDER.indexOf(a.name);
const ordB = COMPONENT_SORT_ORDER.indexOf(b.name);
override disconnectedCallback(): void {
this._sceneData?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
if (ordA === -1) return Infinity;
if (ordB === -1) return -Infinity;
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<SceneDataProvider>>) => {
if (changes.has("selectionComponents"))
this.requestUpdate();
}
return ordA - ordB;
})
.map(({ name, value }) => {
switch (name) {
// TODO?
// case "bevy_core::name::Name": {
// return html`
// <sts-name-inspector
// .value=${value.name}
// ></sts-name-inspector>
// `;
// }
case "bevy_transform::components::transform::Transform": {
return html`
<sts-transform-inspector
.translation=${value.translation}
.rotation=${value.rotation}
.scale=${value.scale}
></sts-transform-inspector>
`;
}
case "bevy_pbr::pbr_material::StandardMaterial": {
const { colorSpace, baseColor } = isLinear(value.base_color)
? { colorSpace: "linear", baseColor: value.base_color.RgbaLinear }
: { colorSpace: "srgb", baseColor: value.base_color.Rgba };
protected override render () {
let components = this._sceneData
?.selectionComponents
.slice()
.sort((a, b) => {
const ordA = COMPONENT_SORT_ORDER.indexOf(a.name);
const ordB = COMPONENT_SORT_ORDER.indexOf(b.name);
if (ordA === -1) return Infinity;
if (ordB === -1) return -Infinity;
return ordA - ordB;
})
.map(({ name, value }) => {
switch (name) {
case "bevy_core::name::Name": {
return html`
<sts-name-inspector
.value=${value.name}
></sts-name-inspector>
`;
return html`
<sts-material-inspector
.baseColor=${baseColor}
.colorSpace=${colorSpace}
.roughness=${value.perceptual_roughness}
.metallic=${value.metallic}
.reflectance=${value.reflectance}
></sts-material-inspector>
`;
}
default: {
return nothing;
// return html`
// <h3>Unknown Component: ${name}</h3>
// <pre>${JSON.stringify({ name, value }, null, " ")}</pre>
// `;
}
}
case "bevy_transform::components::transform::Transform": {
return html`
<sts-transform-inspector
.translation=${value.translation}
.rotation=${value.rotation}
.scale=${value.scale}
></sts-transform-inspector>
`;
}
case "bevy_pbr::pbr_material::StandardMaterial": {
const { colorSpace, baseColor } = isLinear(value.base_color)
? { colorSpace: "linear", baseColor: value.base_color.RgbaLinear }
: { colorSpace: "srgb", baseColor: value.base_color.Rgba };
return html`
<sts-material-inspector
.baseColor=${baseColor}
.colorSpace=${colorSpace}
.roughness=${value.perceptual_roughness}
.metallic=${value.metallic}
.reflectance=${value.reflectance}
></sts-material-inspector>
`;
}
default: {
return nothing;
// return html`
// <h3>Unknown Component: ${name}</h3>
// <pre>${JSON.stringify({ name, value }, null, " ")}</pre>
// `;
}
}
}).filter((value) => value !== nothing) || [];
if (components?.length === 0) {
return nothing;
})
?? nothing
}
return html`
<div class="container">
${components
}
</div>
`;
}
}
const COMPONENT_SORT_ORDER: Array<InspectorComponent["name"]> = [
@@ -11,7 +11,7 @@
h4 {
font-weight: 500;
margin: 0 0 1rem;
margin: 0 0 0.5rem;
}
.fields {
@@ -2,30 +2,41 @@
:host {
display: flex;
gap: 0.25rem;
gap: 0.5rem;
}
.field {
display: flex;
width: math.div(1, 3) * 100%;
border-radius: 0.5rem;
overflow: hidden;
outline: transparent 2px solid {
offset: -2px;
}
transition: outline-color 100ms linear;
&--x:focus-within { outline-color: var(--axis-red) }
&--y:focus-within { outline-color: var(--axis-green) }
&--z:focus-within { outline-color: var(--axis-blue) }
}
.label {
display: flex;
align-items: center;
padding: 0 0.375rem;
font: {
size: math.div(14, 16) * 1rem;
weight: 500;
}
&--x { background: var(--axis-red) }
&--y { background: var(--axis-green) }
&--z { background: var(--axis-blue) }
}
.input {
padding: 0.5rem;
width: math.div(1, 3) * 100%;
border-left-width: 4px;
&:nth-of-type(1) {
border-left-color: rgb(220, 53, 69);
&:focus {
border-left-color: rgb(255, 48, 68);
}
}
&:nth-of-type(2) {
border-left-color: rgb(25, 135, 84);
&:focus {
border-left-color: rgb(0, 187, 100);
}
}
&:nth-of-type(3) {
border-left-color: rgb(13, 110, 253);
&:focus {
border-left-color: rgb(47, 131, 255);
}
}
flex: 1 0 0;
border-radius: 0;
border: none;
outline: none !important;
}
@@ -46,36 +46,44 @@ export class QuatInspectorElement extends LitElement {
}
protected override render = (): TemplateResult => html`
<sts-scalar-field
class="input"
aria-label="X"
format="1.1-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerX}
@value-change=${this.#emitUpdate(RotationAxis.X)}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Y"
format="1.1-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerY}
@value-change=${this.#emitUpdate(RotationAxis.Y)}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Z"
format="1.1-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerZ}
@value-change=${this.#emitUpdate(RotationAxis.Z)}
></sts-scalar-field>
<label class="field field--x">
<span class="label label--x">X</span>
<sts-scalar-field
class="input"
format="1.2-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerX}
@value-change=${this.#emitUpdate(RotationAxis.X)}
></sts-scalar-field>
</label>
<label class="field field--y">
<span class="label label--y">Y</span>
<sts-scalar-field
class="input"
format="1.2-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerY}
@value-change=${this.#emitUpdate(RotationAxis.Y)}
></sts-scalar-field>
</label>
<label class="field field--z">
<span class="label label--z">Z</span>
<sts-scalar-field
class="input"
format="1.2-2 °"
emitOn="change"
.step=${1}
.microStep=${0.1}
.value=${this._eulerZ}
@value-change=${this.#emitUpdate(RotationAxis.Z)}
></sts-scalar-field>
</label>
`;
}
@@ -22,43 +22,57 @@ h4 {
position: relative;
width: 100%;
padding: 0 {
block-start: math.div(14, 16) + 0.333rem !important;
block-start: math.div(17, 16) + 0.25rem !important;
}
margin: 0;
gap: 0.25rem;
gap: 0.5rem;
border: none;
legend {
position: absolute;
inset-block-start: 0;
margin-block-end: 0.333rem;
color: #FFF9;
font-size: math.div(14, 16) * 1rem;
padding: 0;
color: #FFF;
font: {
size: math.div(14, 16) * 1rem;
weight: 600;
}
}
.field {
display: flex;
width: math.div(1, 3) * 100%;
border-radius: 0.5rem;
overflow: hidden;
outline: transparent 2px solid {
offset: -2px;
}
transition: outline-color 100ms linear;
&--x:focus-within { outline-color: var(--axis-red) }
&--y:focus-within { outline-color: var(--axis-green) }
&--z:focus-within { outline-color: var(--axis-blue) }
}
.label {
display: flex;
align-items: center;
padding: 0 0.375rem;
font: {
size: math.div(14, 16) * 1rem;
weight: 500;
}
&--x { background: var(--axis-red) }
&--y { background: var(--axis-green) }
&--z { background: var(--axis-blue) }
}
.input {
padding: 0.5rem;
width: math.div(1, 3) * 100%;
border-left-width: 4px;
&:nth-of-type(1) {
border-left-color: rgb(220, 53, 69);
&:focus {
border-left-color: rgb(255, 48, 68);
}
}
&:nth-of-type(2) {
border-left-color: rgb(25, 135, 84);
&:focus {
border-left-color: rgb(0, 187, 100);
}
}
&:nth-of-type(3) {
border-left-color: rgb(13, 110, 253);
&:focus {
border-left-color: rgb(47, 131, 255);
}
}
flex: 1 0 0;
border-radius: 0;
border: none;
outline: none !important;
}
}
@@ -39,7 +39,6 @@ export class TransformInspectorElement extends LitElement {
}
protected override render = () => html`
<h4>Transform</h4>
<div
class="fieldset"
role="group"
@@ -48,31 +47,44 @@ export class TransformInspectorElement extends LitElement {
<legend id=${this.#positionLabelId}>
Position
</legend>
<sts-scalar-field
class="input"
aria-label="X"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.x}
@value-change=${this.updateField("translation.x")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Y"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.y}
@value-change=${this.updateField("translation.y")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Z"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.z}
@value-change=${this.updateField("translation.z")}
></sts-scalar-field>
<label class="field field--x">
<span class="label label--x">X</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.x}
@value-change=${this.updateField("translation.x")}
></sts-scalar-field>
</label>
<label class="field field--y">
<span class="label label--y">Y</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.y}
@value-change=${this.updateField("translation.y")}
></sts-scalar-field>
</label>
<label class="field field--z">
<span class="label label--z">Z</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.01}
.microStep=${0.001}
.value=${this.translation.z}
@value-change=${this.updateField("translation.z")}
></sts-scalar-field>
</label>
</div>
<div
class="fieldset"
role="group"
@@ -86,6 +98,7 @@ export class TransformInspectorElement extends LitElement {
@value-change=${this.updateField("rotation")}
></sts-quat-inspector>
</div>
<div
class="fieldset"
role="group"
@@ -94,30 +107,42 @@ export class TransformInspectorElement extends LitElement {
<legend id=${this.#scaleLabelId}>
Scale
</legend>
<sts-scalar-field
class="input"
aria-label="X"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.x}
@value-change=${this.updateField("scale.x")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Y"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.y}
@value-change=${this.updateField("scale.y")}
></sts-scalar-field>
<sts-scalar-field
class="input"
aria-label="Z"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.z}
@value-change=${this.updateField("scale.z")}
></sts-scalar-field>
<label class="field field--x">
<span class="label label--x">X</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.x}
@value-change=${this.updateField("scale.x")}
></sts-scalar-field>
</label>
<label class="field field--y">
<span class="label label--y">Y</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.y}
@value-change=${this.updateField("scale.y")}
></sts-scalar-field>
</label>
<label class="field field--z">
<span class="label label--z">Z</span>
<sts-scalar-field
class="input"
format="1.3"
.step=${0.1}
.microStep=${0.01}
.value=${this.scale.z}
@value-change=${this.updateField("scale.z")}
></sts-scalar-field>
</label>
</div>
`;
}
@@ -1,6 +1,6 @@
import * as studio from "@storyteller/studio";
import { SceneElement } from "@storyteller/studio";
import { bind, inject } from "@storyteller/framework";
import { bind, inject, observe } from "@storyteller/framework";
import { DialogBaseElement, DialogElement } from "@storyteller/studio-ui/dialog";
import {
type MediaFile as IMediaFile,
@@ -56,6 +56,7 @@ export class MediaLibraryDialogElement extends DialogBaseElement {
@inject(StorytellerApi)
_api!: StorytellerApi;
@observe(["selectedObject"])
@inject(SCENE_DATA_PROVIDER)
_sceneData!: SceneDataProvider;
@@ -104,22 +105,6 @@ export class MediaLibraryDialogElement extends DialogBaseElement {
return oneAnimationSelected && skeletonSelected;
}
override connectedCallback(): void {
this._sceneData?.addEventListener("property-changes", this.onProviderChanges);
this.loadResults();
super.connectedCallback();
}
override disconnectedCallback(): void {
this._sceneData?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<SceneDataProvider>>) => {
if (changes.has("selectedObject"))
this.requestUpdate();
}
protected override willUpdate(changes: PropertyValues<this>): void {
if (changes.has("_activeCategory") || changes.has("_searchTerm")) {
this._filters = [
+4 -18
View File
@@ -1,6 +1,6 @@
import { inject } from "@storyteller/framework";
import { inject, observe } from "@storyteller/framework";
import * as studio from "@storyteller/studio";
import { LitElement, type PropertyValues, html, unsafeCSS } from "lit";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement } from "lit/decorators.js";
import { SCENE_DATA_PROVIDER, type SceneDataProvider } from "./scene-data-provider.element";
@@ -13,23 +13,9 @@ import styles from "./scene-hierarchy.element.scss?inline";
export class SceneHierarchyElement extends LitElement {
static override styles = unsafeCSS(styles);
@observe(["tree"])
@inject(SCENE_DATA_PROVIDER)
_sceneData?: SceneDataProvider;
override connectedCallback(): void {
this._sceneData?.addEventListener("property-changes", this.onProviderChanges);
super.connectedCallback();
}
override disconnectedCallback(): void {
this._sceneData?.removeEventListener("property-changes", this.onProviderChanges);
super.disconnectedCallback();
}
onProviderChanges = ({ detail: changes }: CustomEvent<PropertyValues<SceneDataProvider>>) => {
if (changes.has("tree"))
this.requestUpdate();
}
_sceneData!: SceneDataProvider;
protected override render = () => html`
${this._sceneData?.tree.map(obj => html`
+31 -12
View File
@@ -1,6 +1,9 @@
import { UniqueToken } from "@storyteller/framework";
import { StudioMode } from "@storyteller/studio";
import { match } from "@storyteller/utility";
export const STUDIO_PARAMS = UniqueToken.create<StudioParams>();
/**
* A thin wrapper around
* {@linkcode https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams URLSearchParams}
@@ -26,42 +29,37 @@ import { match } from "@storyteller/utility";
* - **remote://&lt;media-token&gt;.&lt;file-extension&gt;** - A token for a
* media file that can be fetched from the Storyteller.ai API
*/
export class StudioURLParams {
#_nativeParams?: URLSearchParams;
get #nativeParams() {
return this.#_nativeParams ??= new URLSearchParams(document.location.search);
}
export interface StudioParams {
/**
* Initialize the application from an asset in the "bundled" asset library.
* Takes a filename, including the extension, that can be found under
* `studio/assets/gltf` (e.g., "base-human-female.gltf").
*/
get objectId() { return this.#nativeParams.get("objectId") ?? undefined; }
readonly objectId?: string;
/**
* Initialize the application by retargeting a BVH animation (currently only
* supports the MocapNET skeleton) onto the bundled `Mannequin.gltf` model.
*/
get bvh() { return this.#nativeParams.get("bvh") ?? undefined; }
readonly bvh?: string;
/**
* Initialize the application by retargeting a Mixamo animation onto the
* bundled `Mannequin.gltf` model.
*/
get mixamo() { return this.#nativeParams.get("mixamo") ?? undefined; }
readonly mixamo?: string;
/**
* Initialize the application by loading an arbitrary glTF scene from any
* source.
*/
get sceneImport() { return this.#nativeParams.get("sceneImport") ?? undefined; }
readonly sceneImport?: string;
/**
* Initialize the application by loading a previously saved Storyteller
* Studio scene in `*.scn.ron` format.
*/
get scene() { return this.#nativeParams.get("scene") ?? undefined; }
readonly scene?: string;
/**
* One of:
@@ -70,7 +68,7 @@ export class StudioURLParams {
* "specular" qualifiers (e.g., "gum_trees_4k").
* - A hex-formatted color, without the leading `#` (e.g., "1A1A27")
*/
get skybox() { return this.#nativeParams.get("skybox") ?? undefined; }
readonly skybox?: string;
/**
* Determines whether the app initializes in a minimal, read-only "viewer"
@@ -82,6 +80,27 @@ export class StudioURLParams {
*
* @default StudioMode.Viewer
*/
readonly mode: StudioMode;
}
export class StudioURLParams implements StudioParams {
#_nativeParams?: URLSearchParams;
get #nativeParams() {
return this.#_nativeParams ??= new URLSearchParams(document.location.search);
}
get objectId() { return this.#nativeParams.get("objectId") ?? undefined; }
get bvh() { return this.#nativeParams.get("bvh") ?? undefined; }
get mixamo() { return this.#nativeParams.get("mixamo") ?? undefined; }
get sceneImport() { return this.#nativeParams.get("sceneImport") ?? undefined; }
get scene() { return this.#nativeParams.get("scene") ?? undefined; }
get skybox() { return this.#nativeParams.get("skybox") ?? undefined; }
get mode(): StudioMode {
return match(this.#nativeParams.get("mode"), {
"studio": () => StudioMode.Editor,
+11 -4
View File
@@ -1164,10 +1164,16 @@ mod wasm {
};
}
export interface AnimationTrack {
name?: string;
targetType: SceneElement;
animationTarget: AnimationTarget;
}
export interface UpdateAnimationTargets extends CustomEvent {
type: "update-animation-targets",
detail: {
added: Record<string, { name?: string, target_type: SceneElement, animation_target: AnimationTarget } >,
added: Record<string, AnimationTrack>,
removed: string[]
}
}
@@ -1177,9 +1183,9 @@ mod wasm {
time: number,
entity: string,
incoming_easing: "linear" | "cubic-hermite",
affect_translation: bool,
affect_rotation: bool,
affect_scale: bool
affect_translation: boolean,
affect_rotation: boolean,
affect_scale: boolean
}
export interface TimedAnimationClipInfo {
@@ -1221,6 +1227,7 @@ mod wasm {
"#;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AddedAnimationTargetRecordItem {
name: Option<String>,
target_type: SceneElement,
+2
View File
@@ -316,6 +316,8 @@ fn initialize(mut cmd: Commands) {
..default()
},
Fxaa::default(),
// FIXME: This should not be the user-visible/keyframable camera
SceneElement::Camera,
));
}
+3 -1
View File
@@ -27,8 +27,10 @@
"@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/radio": ["studio-ui/src/lib/forms/radio/index.ts"],
"@storyteller/studio-ui/forms/scalar-field": ["studio-ui/src/lib/forms/scalar-field.element.ts"],
"@storyteller/studio-ui/forms/select": ["studio-ui/src/lib/forms/select/index.ts"],
"@storyteller/studio-ui/forms/text-field": ["studio-ui/src/lib/forms/text-field.element.ts"],
"@storyteller/studio-web": ["studio-web/src/index.ts"],
"@storyteller/studio-web/add-to-scene-menu": ["studio-web/src/lib/add-to-scene-menu.element.ts"],
"@storyteller/studio-web/canvas-provider": ["studio-web/src/lib/canvas-provider.element.ts"],