mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
move & update keyframes
This commit is contained in:
@@ -34,7 +34,11 @@
|
||||
background: #7A2C34E3;
|
||||
color: #FFF;
|
||||
|
||||
&:hover {
|
||||
&.disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&:not(.disabled):hover {
|
||||
background: #90363F;
|
||||
}
|
||||
}
|
||||
@@ -95,4 +99,21 @@
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-row: 2 / span var(--track-count);
|
||||
|
||||
|
||||
.track {
|
||||
&:hover {
|
||||
background: #0000001F;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #7A2C34E3;
|
||||
color: #FFF;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: #90363F !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as studio from "@storyteller/studio";
|
||||
import { type CameraKeyframesEvent, type SeekTimeChanged, type UpdateAnimationTargets } from "@storyteller/studio";
|
||||
import type { EntitySelectEvent, SeekTimeChanged, UpdateAnimationTargets } from "@storyteller/studio";
|
||||
import { bind, drag, on } from "@storyteller/framework";
|
||||
import { LitElement, type PropertyValues, html, unsafeCSS, TemplateResult, PropertyValueMap } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
@@ -37,9 +37,9 @@ export class AnimEditorElement extends LitElement {
|
||||
|
||||
@state() track_ordered : string[] = [];
|
||||
|
||||
@state() keyframes : Record<string, number[]> = {};
|
||||
@state() keyframes : Record<string, Keyframe> = {};
|
||||
|
||||
@state() selected_track : string | null = null;
|
||||
@state() selected_track : string | false = false;
|
||||
|
||||
trackCount(): number {
|
||||
return Object.keys(this.tracks).length;
|
||||
@@ -81,12 +81,39 @@ export class AnimEditorElement extends LitElement {
|
||||
this.tracks = tracks;
|
||||
}
|
||||
|
||||
@on("window:update-keyframes")
|
||||
onUpdateKeyframes({detail: {added, removed}}: studio.UpdateKeyframes): void {
|
||||
let addedKeyframes = Object.keys(added);
|
||||
if (addedKeyframes.length === 0 && removed.length === 0) {
|
||||
return;
|
||||
}
|
||||
console.log("Keyframes Changed");
|
||||
let keyframes = {...this.keyframes};
|
||||
for (let keyframe of addedKeyframes) {
|
||||
keyframes[keyframe] = added[keyframe];
|
||||
}
|
||||
|
||||
this.keyframes = keyframes;
|
||||
}
|
||||
|
||||
|
||||
@on("window:entity-select")
|
||||
onEntitySelected({ detail: id }: EntitySelectEvent): void {
|
||||
if (id && this.tracks[id]) {
|
||||
this.selected_track = id;
|
||||
} else {
|
||||
this.selected_track = false;
|
||||
}
|
||||
}
|
||||
|
||||
onSidebarResize(event: PointerEvent): void {
|
||||
this.sidebarWidth = Math.max(Math.min(this.sidebarWidth + event.movementX, 512), 128);
|
||||
}
|
||||
|
||||
addKeyframe(): void {
|
||||
studio.addCameraKeyframe(this.playHead);
|
||||
if (this.selected_track) {
|
||||
studio.recordKeyframe([this.selected_track]);
|
||||
}
|
||||
}
|
||||
|
||||
jumpToStart(): void {
|
||||
@@ -123,6 +150,31 @@ export class AnimEditorElement extends LitElement {
|
||||
'--track-count': `${this.trackCount()}`,
|
||||
'--row-count': `calc(var(--track-count) + 1)`
|
||||
};
|
||||
|
||||
const track_order_hashed : Record<string, number> = {};
|
||||
|
||||
for (const [index, track] of this.track_ordered.entries()) {
|
||||
track_order_hashed[track] = index;
|
||||
}
|
||||
|
||||
const tracklist = this.track_ordered.map((entity) => {
|
||||
let track = this.tracks[entity];
|
||||
let name = track.name ?? track.target_type;
|
||||
let icon = TRACK_ICONS[track.target_type] ?? "minus";
|
||||
return html`
|
||||
<sts-tree class=${`track ` + (this.selected_track === entity ? `selected` : ``)}
|
||||
role="listitem"
|
||||
name=${name}
|
||||
icon=${icon}
|
||||
@click=${() => {
|
||||
studio.select(entity);
|
||||
}}
|
||||
></sts-tree>
|
||||
`;
|
||||
});
|
||||
|
||||
const none_selected = !this.selected_track;
|
||||
|
||||
return html`
|
||||
<div class="sidebar"
|
||||
style=${styleMap({
|
||||
@@ -136,6 +188,7 @@ export class AnimEditorElement extends LitElement {
|
||||
icon="key"
|
||||
aria-label="Add Keyframe"
|
||||
title="Add Keyframe"
|
||||
?disabled=${none_selected}
|
||||
@click=${this.addKeyframe}
|
||||
></sts-button>
|
||||
</sts-toolbar-group>
|
||||
@@ -173,18 +226,7 @@ export class AnimEditorElement extends LitElement {
|
||||
</sts-toolbar-group>
|
||||
</sts-toolbar>
|
||||
<div class="tracklist" role="list">
|
||||
${
|
||||
this.track_ordered.map((entity) => {
|
||||
let track = this.tracks[entity];
|
||||
let name = track.name ?? track.target_type;
|
||||
let icon = TRACK_ICONS[track.target_type] ?? "minus";
|
||||
return html!`<sts-tree class="track"
|
||||
role="listitem"
|
||||
name=${name}
|
||||
icon=${icon}
|
||||
></sts-tree>`;
|
||||
})
|
||||
}
|
||||
${tracklist}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -202,6 +244,7 @@ export class AnimEditorElement extends LitElement {
|
||||
.clipEnd=${this.clipEnd}
|
||||
.playHead=${bind(this, "playHead")}
|
||||
.keyframes=${bind(this, "keyframes")}
|
||||
.track_ordered=${track_order_hashed}
|
||||
></sts-anim-timeline>
|
||||
`
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
:host {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-rows: subgrid;
|
||||
grid-row: 1 / span var(--row-count);
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
@@ -8,21 +11,18 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: subgrid;
|
||||
inset: 0;
|
||||
grid-row: 1 / span var(--row-count);
|
||||
|
||||
&.panning {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.clip-region {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: #0004;
|
||||
}
|
||||
|
||||
.hash {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
.timestamp {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
grid-row: 1;
|
||||
color: #FFF6;
|
||||
font: {
|
||||
family: "Fira Sans", sans-serif;
|
||||
@@ -100,8 +100,7 @@
|
||||
|
||||
.keyframe {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateX(-4.5px) translateY(-4.5px);
|
||||
transform: translateX(-4.5px);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
@@ -62,7 +62,8 @@ export class AnimTimelineElement extends LitElement {
|
||||
/** The time (in seconds) of the current play-head position */
|
||||
@property({ type: Number }) playHead = 0;
|
||||
/** Keyframe positions (in seconds) */
|
||||
@property({ attribute: false }) keyframes: Record<string, number> = {};
|
||||
@property({ attribute: false }) keyframes: Record<string, studio.Keyframe> = {};
|
||||
@property({ attribute: false }) track_ordered: Record<string, number> = {}
|
||||
|
||||
@state() _selectedKeyframe: string | null = null;
|
||||
|
||||
@@ -131,8 +132,9 @@ export class AnimTimelineElement extends LitElement {
|
||||
}
|
||||
|
||||
if (Array.from(Object.keys(this.keyframes)).includes(id)) {
|
||||
let keyframe = this.keyframes[id];
|
||||
this._selectedKeyframe = id;
|
||||
this.dispatchEvent(new CustomEvent("playHead-change", { detail: this.keyframes[id]! }));
|
||||
studio.seekTime(keyframe.time);
|
||||
} else {
|
||||
this._selectedKeyframe = null;
|
||||
}
|
||||
@@ -190,20 +192,12 @@ export class AnimTimelineElement extends LitElement {
|
||||
}
|
||||
|
||||
onKeyframeDrag(id: string, event: PointerEvent): void {
|
||||
const deltaSeconds = event.movementX * this.secondsPerPx.get();
|
||||
const updated = this.keyframes[id] + deltaSeconds;
|
||||
let elementBounds = this.getBoundingClientRect();
|
||||
let relativeX = event.clientX - elementBounds.left;
|
||||
let ratioX = Math.max(0, Math.min(relativeX / elementBounds.width, 1));
|
||||
const time = (this.rangeEnd - this.rangeStart) * ratioX + this.rangeStart;
|
||||
|
||||
studio.updateKeyframe(id, updated);
|
||||
|
||||
const updatedKeyframes = Object.fromEntries(
|
||||
Object
|
||||
.entries(this.keyframes)
|
||||
.map(entry => entry[0] === id
|
||||
? [id, updated]
|
||||
: entry
|
||||
)
|
||||
);
|
||||
this.dispatchEvent(new CustomEvent("keyframes-change", { detail: updatedKeyframes }));
|
||||
studio.moveKeyframeTime(id, time);
|
||||
}
|
||||
|
||||
#updateRenderWidth(): void {
|
||||
@@ -296,10 +290,6 @@ export class AnimTimelineElement extends LitElement {
|
||||
})}
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="clip-region"
|
||||
style=${styleMap({
|
||||
})}
|
||||
></div>
|
||||
|
||||
${subdivisions.map(({ seconds, fraction, isMajor}, idx) => {
|
||||
const secondsPercentage = fraction * 100;
|
||||
@@ -342,14 +332,17 @@ export class AnimTimelineElement extends LitElement {
|
||||
${repeat(
|
||||
Object.entries(this.keyframes),
|
||||
([id]) => id,
|
||||
([id, keyframe]) => html`
|
||||
([id, keyframe]) => {
|
||||
let row = this.track_ordered[keyframe.target];
|
||||
return html`
|
||||
<div id=${id}
|
||||
class=${classMap({
|
||||
keyframe: true,
|
||||
selected: this._selectedKeyframe === id,
|
||||
selected: this._selectedKeyframe === id
|
||||
})}
|
||||
style=${styleMap({
|
||||
left: `${100 * (keyframe - this.rangeStart) / range}%`
|
||||
left: `${100 * (keyframe.time - this.rangeStart) / range}%`,
|
||||
gridRow: row + 2,
|
||||
})}
|
||||
tabindex="-1"
|
||||
${drag({
|
||||
@@ -357,7 +350,8 @@ export class AnimTimelineElement extends LitElement {
|
||||
})}
|
||||
@click=${() => this.selectKeyframe(id)}
|
||||
></div>
|
||||
`
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user