mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-20 07:54:50 +00:00
UI styling, main workspace layout (#119)
This commit is contained in:
@@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user