tracks move correctly

This commit is contained in:
Lee-Orr
2024-03-12 13:36:58 -04:00
parent fc3ed85192
commit 23aafc174c
3 changed files with 154 additions and 118 deletions
@@ -1,21 +1,27 @@
:host { :host {
display: flex; display: grid;
border-radius: 8px; border-radius: 8px;
background: #0006; background: #0006;
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
grid-template-columns: [sidebar-start] min-content [sidebar-end divider-start] 8px [divider-end timeline-start] 1fr [timeline-end];
padding: 8px;
row-gap: 8px;
} }
.sidebar, .timeline { padding: 8px; }
.sidebar { padding-inline-end: 0; }
.timeline { padding-inline-start: 0; }
.sidebar { .sidebar {
flex: 0 0 auto; flex: 0 0 auto;
width: 256px; width: 256px;
display: flex; display: grid;
flex-direction: column;
gap: 8px;
overflow: hidden; overflow: hidden;
grid-column: sidebar;
grid-row: 1 / span var(--row-count);
grid-template-rows: subgrid;
}
.timeline {
grid-column: timeline;
grid-row: 1 / span var(--row-count);
grid-template-rows: subgrid;
} }
.btn-keyframe { .btn-keyframe {
@@ -58,6 +64,8 @@
} }
.divider { .divider {
grid-column: divider;
grid-row: 1 / span var(--row-count);
position: relative; position: relative;
flex: 0 0 8px; flex: 0 0 8px;
margin: { margin: {
@@ -83,6 +91,8 @@
} }
} }
.timeline { .tracklist {
flex: 1 0 0; display: grid;
grid-template-columns: subgrid;
grid-row: 2 / span var(--track-count);
} }
+63 -36
View File
@@ -1,7 +1,7 @@
import * as studio from "@storyteller/studio"; import * as studio from "@storyteller/studio";
import { type CameraKeyframesEvent, type SeekTimeChanged, type UpdateAnimationTargets } from "@storyteller/studio"; import { type CameraKeyframesEvent, type SeekTimeChanged, type UpdateAnimationTargets } from "@storyteller/studio";
import { bind, drag, on } from "@storyteller/framework"; import { bind, drag, on } from "@storyteller/framework";
import { LitElement, type PropertyValues, html, unsafeCSS, TemplateResult } from "lit"; import { LitElement, type PropertyValues, html, unsafeCSS, TemplateResult, PropertyValueMap } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js"; import { styleMap } from "lit/directives/style-map.js";
@@ -12,6 +12,12 @@ import "./anim-timeline.element";
import styles from "./anim-editor.element.scss?inline"; import styles from "./anim-editor.element.scss?inline";
const TRACK_ICONS : Record<string, string> = {
camera: "video",
transform: "up-down-left-right",
light: "light"
};
@customElement("sts-anim-editor") @customElement("sts-anim-editor")
export class AnimEditorElement extends LitElement { export class AnimEditorElement extends LitElement {
static override styles = unsafeCSS(styles); static override styles = unsafeCSS(styles);
@@ -19,14 +25,25 @@ export class AnimEditorElement extends LitElement {
@state() sidebarWidth = 256; @state() sidebarWidth = 256;
@state() timelineStart = 0; @state() timelineStart = 0;
@state() timelineEnd = 5_000; @state() timelineEnd = 5;
@state() clipStart = 0; @state() clipStart = 0;
@state() clipEnd = 15_000; @state() clipEnd = 15_000;
@state() playHead = 0; @state() playHead = 0;
@state() playing = false; @state() playing = false;
@state() keyframes: Record<string, number> = {};
@state() tracks : Record<string, {name?: string, target_type: string }> = {};
@state() track_ordered : string[] = [];
@state() keyframes : Record<string, number[]> = {};
@state() selected_track : string | null = null;
trackCount(): number {
return Object.keys(this.tracks).length;
}
protected override update(changes: PropertyValues<this>): void { protected override update(changes: PropertyValues<this>): void {
if (changes.has("playHead")) if (changes.has("playHead"))
@@ -42,14 +59,26 @@ export class AnimEditorElement extends LitElement {
this.playing = playing; this.playing = playing;
} }
@on("window:camera-keyframes")
onCameraKeyframeChanges({ detail: keyframes }: CameraKeyframesEvent): void {
this.keyframes = keyframes;
}
@on("window:update-animation-targets") @on("window:update-animation-targets")
onUpdateAnimationTargets({detail: { added, removed}}: UpdateAnimationTargets) : void { onUpdateAnimationTargets({detail: { added, removed}}: UpdateAnimationTargets) : void {
let addedKeys = Object.keys(added);
if (addedKeys.length === 0 && removed.length === 0) {
return;
}
console.log("Animation Targets Changed");
let tracks = {...this.tracks};
for (let track_entity of addedKeys) {
let track = added[track_entity];
tracks[track_entity] = track;
}
for (let track_entity of removed) {
delete tracks[track_entity];
}
this.track_ordered = Object.keys(tracks);
this.tracks = tracks;
} }
onSidebarResize(event: PointerEvent): void { onSidebarResize(event: PointerEvent): void {
@@ -67,26 +96,12 @@ export class AnimEditorElement extends LitElement {
prevKeyframe(): void { prevKeyframe(): void {
let pos = this.playHead; let pos = this.playHead;
const keyframes = Array.from(Object.values(this.keyframes)).sort((a, b) => b - a);
for (let keyframe of keyframes) {
pos = keyframe;
if (keyframe < this.playHead)
break;
}
this.playHead = pos; this.playHead = pos;
} }
nextKeyframe(): void { nextKeyframe(): void {
let pos = this.playHead; let pos = this.playHead;
const keyframes = Array.from(Object.values(this.keyframes)).sort((a, b) => a - b);
for (let keyframe of keyframes) {
pos = keyframe;
if (keyframe > this.playHead)
break;
}
this.playHead = pos; this.playHead = pos;
} }
@@ -95,6 +110,7 @@ export class AnimEditorElement extends LitElement {
} }
togglePlaying(): void { togglePlaying(): void {
console.log("toggling play");
if (this.playing) { if (this.playing) {
studio.pause(); studio.pause();
} else { } else {
@@ -102,10 +118,16 @@ export class AnimEditorElement extends LitElement {
} }
} }
protected override render = (): TemplateResult => html` protected override render(): TemplateResult {
let track_count = {
'--track-count': `${this.trackCount()}`,
'--row-count': `calc(var(--track-count) + 1)`
};
return html`
<div class="sidebar" <div class="sidebar"
style=${styleMap({ style=${styleMap({
width: `${this.sidebarWidth}px`, width: `${this.sidebarWidth}px`,
...track_count
})} })}
> >
<sts-toolbar> <sts-toolbar>
@@ -151,26 +173,29 @@ export class AnimEditorElement extends LitElement {
</sts-toolbar-group> </sts-toolbar-group>
</sts-toolbar> </sts-toolbar>
<div class="tracklist" role="list"> <div class="tracklist" role="list">
<sts-tree class="track" ${
role="listitem" this.track_ordered.map((entity) => {
name="Camera" let track = this.tracks[entity];
icon="video" let name = track.name ?? track.target_type;
></sts-tree> let icon = TRACK_ICONS[track.target_type] ?? "minus";
<sts-tree class="track" return html!`<sts-tree class="track"
role="listitem" role="listitem"
name="Camera Target" name=${name}
icon="crosshairs" icon=${icon}
></sts-tree> ></sts-tree>`;
})
}
</div> </div>
</div> </div>
<div class="divider" <div class="divider" style=${styleMap(track_count)}
${drag({ ${drag({
move: this.onSidebarResize, move: this.onSidebarResize,
})} })}
></div> ></div>
<sts-anim-timeline class="timeline" <sts-anim-timeline class="timeline"
style=${styleMap(track_count)}
.rangeStart=${bind(this, "timelineStart")} .rangeStart=${bind(this, "timelineStart")}
.rangeEnd=${bind(this, "timelineEnd")} .rangeEnd=${bind(this, "timelineEnd")}
.clipStart=${this.clipStart} .clipStart=${this.clipStart}
@@ -178,5 +203,7 @@ export class AnimEditorElement extends LitElement {
.playHead=${bind(this, "playHead")} .playHead=${bind(this, "playHead")}
.keyframes=${bind(this, "keyframes")} .keyframes=${bind(this, "keyframes")}
></sts-anim-timeline> ></sts-anim-timeline>
`; `
} };
};
@@ -59,11 +59,6 @@ export class AnimTimelineElement extends LitElement {
/** The time (in seconds) at the right edge of the timeline view */ /** The time (in seconds) at the right edge of the timeline view */
@property({ type: Number }) rangeEnd = 5; @property({ type: Number }) rangeEnd = 5;
/** The time (in seconds) at the beginning of the active clip range */
@property({ type: Number }) clipStart = 0;
/** The time (in seconds) at the end of the active clip range */
@property({ type: Number }) clipEnd = 15;
/** The time (in seconds) of the current play-head position */ /** The time (in seconds) of the current play-head position */
@property({ type: Number }) playHead = 0; @property({ type: Number }) playHead = 0;
/** Keyframe positions (in seconds) */ /** Keyframe positions (in seconds) */
@@ -86,7 +81,7 @@ export class AnimTimelineElement extends LitElement {
@state() _panning = false; @state() _panning = false;
/** How many seconds are represented by each px of horizontal space. */ /** How many seconds are represented by each px of horizontal space. */
pxPerSecond = new Computed(() => (this.rangeEnd - this.rangeStart) / this._renderWidth); secondsPerPx = new Computed(() => (this.rangeEnd - this.rangeStart) / this._renderWidth);
#resizeObserver: ResizeObserver; #resizeObserver: ResizeObserver;
@@ -107,12 +102,13 @@ export class AnimTimelineElement extends LitElement {
} }
protected override update(changes: PropertyValues<this>): void { protected override update(changes: PropertyValues<this>): void {
console.log("Changes: ", changes);
if ( if (
changes.has("rangeStart") changes.has("rangeStart")
|| changes.has("rangeEnd") || changes.has("rangeEnd")
|| changes.has("_renderWidth") || changes.has("_renderWidth")
) { ) {
this.pxPerSecond.invalidate(); this.secondsPerPx.invalidate();
} }
super.update(changes); super.update(changes);
@@ -158,7 +154,7 @@ export class AnimTimelineElement extends LitElement {
// FIXME: When zooming the timeline, the transform origin should be // FIXME: When zooming the timeline, the transform origin should be
// centered on the mouse cursor. Currently this does not work as // centered on the mouse cursor. Currently this does not work as
// intended, always centering on the 00:00 timestamp instead. // intended, always centering on the 00:00 timestamp instead.
const t = ((this._offsetX + origin) * this.pxPerSecond.get()) / currentRange; const t = ((this._offsetX + origin) * this.secondsPerPx.get()) / currentRange;
console.log(`t: ${t}`); console.log(`t: ${t}`);
console.log(`deltaStart: ${-deltaRange * t}`); console.log(`deltaStart: ${-deltaRange * t}`);
console.log(`deltaEnd: ${deltaRange * (1 - t)}`); console.log(`deltaEnd: ${deltaRange * (1 - t)}`);
@@ -169,22 +165,22 @@ export class AnimTimelineElement extends LitElement {
this.dispatchEvent(new CustomEvent("rangeEnd-change", { detail: rangeEnd })); this.dispatchEvent(new CustomEvent("rangeEnd-change", { detail: rangeEnd }));
} }
#onDragStart(): void { onDragStart(): void {
this._panning = true; this._panning = true;
} }
#onDragEnd(): void { onDragEnd(): void {
this._panning = false; this._panning = false;
} }
#onDrag(event: PointerEvent): void { onDrag(event: PointerEvent): void {
this.#pan(event.movementX); this.#pan(event.movementX);
} }
#pan(deltaPx: number): void { #pan(deltaPx: number): void {
this._offsetX += deltaPx; this._offsetX += deltaPx;
const pxPerSecond = this.pxPerSecond.get(); const pxPerSecond = this.secondsPerPx.get();
const rangeStart = this.rangeStart - deltaPx * pxPerSecond; const rangeStart = this.rangeStart - deltaPx * pxPerSecond;
const rangeEnd = this.rangeEnd - deltaPx * pxPerSecond; const rangeEnd = this.rangeEnd - deltaPx * pxPerSecond;
@@ -192,15 +188,18 @@ export class AnimTimelineElement extends LitElement {
this.dispatchEvent(new CustomEvent("rangeEnd-change", { detail: rangeEnd })); this.dispatchEvent(new CustomEvent("rangeEnd-change", { detail: rangeEnd }));
} }
#onPlayheadDrag(event: PointerEvent): void { onPlayheadDrag(event: PointerEvent): void {
const deltaSeconds = event.movementX * this.pxPerSecond.get(); let elementBounds = this.getBoundingClientRect();
const updated = this.playHead + deltaSeconds; let relativeX = event.clientX - elementBounds.left;
let ratioX = Math.max(0, Math.min(relativeX / elementBounds.width, 1));
const seconds = (this.rangeEnd - this.rangeStart) * ratioX + this.rangeStart;
const updated = seconds;
this.dispatchEvent(new CustomEvent("playHead-change", { detail: updated })); this.dispatchEvent(new CustomEvent("playHead-change", { detail: updated }));
} }
#onKeyframeDrag(id: string, event: PointerEvent): void { onKeyframeDrag(id: string, event: PointerEvent): void {
const deltaSeconds = event.movementX * this.pxPerSecond.get(); const deltaSeconds = event.movementX * this.secondsPerPx.get();
const updated = this.keyframes[id] + deltaSeconds; const updated = this.keyframes[id] + deltaSeconds;
studio.updateKeyframe(id, updated); studio.updateKeyframe(id, updated);
@@ -223,59 +222,65 @@ export class AnimTimelineElement extends LitElement {
/** /**
* Format a time value (in seconds) for displaying in the timeline view * Format a time value (in seconds) for displaying in the timeline view
*/ */
#timestamp(seconds: number) { timestamp(seconds: number) {
const sign = seconds < 0 ? "-" : ""; const sign = seconds < 0 ? "-" : "";
seconds = Math.abs(seconds); seconds = Math.abs(seconds);
let s = Math.floor(seconds); let minutes = Math.floor(seconds / 60);
let frac = Math.floor(seconds * 1000 - s * 1000); let secondsInMinute = seconds % 60;
if (frac === 1000) { let flooredSeconds = Math.floor(secondsInMinute);
frac = 0; let fraction = Math.floor(100 * (secondsInMinute - flooredSeconds));
s += 1;
}
let m = 0;
if (s >= 60) {
m = Math.floor(s / 60);
s %= 60;
}
return html`${ return html`${
sign sign
}${ }${
m.toString(10).padStart(2, "0") minutes.toString(10).padStart(2, "0")
}:${ }:${
s.toString(10).padStart(2, "0") flooredSeconds.toString(10).padStart(2, "0")
}<!--<span class="frac">.${ }.${
frac.toString(10).padEnd(3, "0") fraction.toString(10).padStart(2, "0")
}</span>-->`; }`;
} }
/** /**
* Convert a time value (in seconds) to a px offset from the left edge of the view * Convert a time value (in seconds) to a px offset from the left edge of the view
*/ */
#secondsToPx(seconds: number): number { secondsToPx(seconds: number): number {
return this._offsetX + seconds * this.pxPerSecond.get(); return this._offsetX + seconds * this.secondsPerPx.get();
} }
protected override render = () => { protected override render() {
const pxPerSecond = this.pxPerSecond.get(); const {
subdivision: subdivisionScale,
major: majorSubdivisionScale,
} = {
subdivision: 0.5,
major: 1,
};
const scaledStart = this.rangeStart / subdivisionScale;
const scaledEnd = this.rangeEnd / subdivisionScale;
const range = this.rangeEnd - this.rangeStart; const range = this.rangeEnd - this.rangeStart;
const subdivsCount = Math.floor(this._renderWidth / 64); console.log("Scaled Start", scaledStart, "Scaled End", scaledEnd, "Range", range);
const pxPerSubdiv = this._renderWidth / subdivsCount;
const secondsPerSubdiv = pxPerSecond / pxPerSubdiv;
console.log("Seconds per Subdiv: ", secondsPerSubdiv); let subdivisions : Array<{seconds: number, fraction: number, isMajor: boolean}> = [];
const subdivs = new Array(subdivsCount + 2).fill(null); let currentOffset = scaledStart;
let currentOffsetSeconds = this.rangeStart;
const fracPxOffset = this._offsetX % pxPerSubdiv; while (currentOffset < scaledEnd) {
const currentSeconds = currentOffsetSeconds;
const isMajor = currentOffsetSeconds % majorSubdivisionScale === 0;
subdivisions.push({ seconds: currentSeconds, fraction: currentSeconds / range, isMajor });
let nextSubdiv = Math.ceil(currentOffset);
if (Math.abs(nextSubdiv - currentOffset) < 0.1) {
nextSubdiv += 1;
}
currentOffset = nextSubdiv;
currentOffsetSeconds = currentOffset * subdivisionScale;
}
let startingIdx = -Math.floor(this._offsetX / pxPerSubdiv); console.log("Subdivisions", subdivisions.length, subdivisions);
if (fracPxOffset < 0)
startingIdx -= 2;
else
startingIdx -= 1;
return html` return html`
<div <div
@@ -285,43 +290,37 @@ export class AnimTimelineElement extends LitElement {
})} })}
${drag({ ${drag({
button: 1, button: 1,
start: this.#onDragStart, start: this.onDragStart,
move: this.#onDrag, move: this.onDrag,
end: this.#onDragEnd, end: this.onDragEnd,
})} })}
tabindex="-1" tabindex="-1"
> >
<div class="clip-region" <div class="clip-region"
style=${styleMap({ style=${styleMap({
left: `${this.#secondsToPx(this.clipStart)}px`,
right: `${this._renderWidth - this.#secondsToPx(this.clipEnd)}px`,
})} })}
></div> ></div>
${subdivs.map((_, idx) => { ${subdivisions.map(({ seconds, fraction, isMajor}, idx) => {
const pxOffset = (idx - 1) * pxPerSubdiv + fracPxOffset; const secondsPercentage = fraction * 100;
const seconds = (startingIdx + idx) * secondsPerSubdiv;
const secondsFrac = Math.abs(Math.round(seconds));
const strong = secondsFrac === 0;
return html` return html`
<div <div
class=${classMap({ class=${classMap({
hash: true, hash: true,
strong, strong: isMajor,
})} })}
style=${styleMap({ style=${styleMap({
left: `${pxOffset}px` left: `${secondsPercentage}%`
})} })}
></div> ></div>
${strong ? html` ${true ? html`
<span class="timestamp" <span class="timestamp"
style=${styleMap({ style=${styleMap({
left: `${pxOffset + 8}px` left: `calc(${secondsPercentage}% + 8px)`
})} })}
> >
${seconds} ${this.timestamp(seconds)}
</span> </span>
` : nothing} ` : nothing}
`; `;
@@ -329,10 +328,10 @@ export class AnimTimelineElement extends LitElement {
<div class="playhead" <div class="playhead"
${drag({ ${drag({
move: this.#onPlayheadDrag move: this.onPlayheadDrag
})} })}
style=${styleMap({ style=${styleMap({
left: `${this.#secondsToPx(this.playHead)}px` left: `${100 * (this.playHead - this.rangeStart) / range}%`
})} })}
tabindex="-1" tabindex="-1"
> >
@@ -350,11 +349,11 @@ export class AnimTimelineElement extends LitElement {
selected: this._selectedKeyframe === id, selected: this._selectedKeyframe === id,
})} })}
style=${styleMap({ style=${styleMap({
left: `${this.#secondsToPx(keyframe)}px` left: `${this.secondsToPx(keyframe)}px`
})} })}
tabindex="-1" tabindex="-1"
${drag({ ${drag({
move: event => this.#onKeyframeDrag(id, event), move: event => this.onKeyframeDrag(id, event),
})} })}
@click=${() => this.selectKeyframe(id)} @click=${() => this.selectKeyframe(id)}
></div> ></div>