mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
Animation Editor UI (#20)
This PR adds a WIP timeline UI for animating elements in the scene. ### Implemented Features * The timeline can be panned with MMB and zoomed with Ctrl + Mouse Wheel * The timeline can be "scrubbed" by dragging the red "play-head" with LMB * The timeline sidebar (displaying the list of tracks) can be resized by dragging on the edge of the sidebar * Keyframes can be added by pressing the "Add Keyframe" CTA in the UI * Keyframes can be selected for editing by clicking the diamond-shaped markers in the timeline * Selected entities (including but not limited to keyframes) can be deleted by pressing the "Delete" key * Keyframes can be moved temporally by dragging the diamond-shaped markers in the timeline ### Known issues * Zooming the timeline in too far results in a soft-lock * When zooming the timeline, the "transform origin" is currently centered on the `00:00` timestamp. For ease of use, it should center on the mouse cursor position instead. * The timeline track-list is currently hard-coded to list the Camera and Camera Target. Clicking on these entries currently serves no useful purpose and may result in a crash. * Keyframe markers in the timeline are not currently associated with entries in the track-list and simply appear vertically centered in the timeline view. * Added keyframes currently record the position and orientation of the scene camera when added, and are rendered in the world with debug visualizations, but serve no useful purpose. Some design work is needed (both user-experience and architectural) to decide on a workflow for actually animating scene elements. * Entity deletion cannot currently be undone/redone
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from "./lib/decorators";
|
||||
export * from "./lib/di";
|
||||
export { default as spread } from "./lib/directives/spread";
|
||||
export { default as drag, type Props as DragProps } from "./lib/directives/drag";
|
||||
|
||||
@@ -65,7 +65,7 @@ export class ProviderRemoval<T> extends CustomEvent<Provider<T>> {
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
[DIEvent.InjectionRequest]: InjectionRequest<any>;
|
||||
[DIEvent.DependencyProvision]: DependencyProvision<any>;
|
||||
[DIEvent.ProviderRemoval]: ProviderRemoval<any>;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ElementPart, noChange, nothing } from "lit";
|
||||
import { directive, Directive } from "lit/directive.js";
|
||||
|
||||
export interface Props {
|
||||
button?: number;
|
||||
start?(event: PointerEvent): void;
|
||||
move?(event: PointerEvent): void;
|
||||
end?(event: PointerEvent): void;
|
||||
}
|
||||
|
||||
class Drag extends Directive {
|
||||
#initialized = false;
|
||||
#pointerButton = 0;
|
||||
|
||||
#startHandler?: (event: PointerEvent) => void;
|
||||
#moveHandler?: (event: PointerEvent) => void;
|
||||
|
||||
#lastProps: Props = {};
|
||||
|
||||
override render(_: Props) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
override update(part: ElementPart, [props]: [Props]) {
|
||||
const element = part.element as HTMLElement;
|
||||
const host = part.options?.host;
|
||||
|
||||
// Disable the default drag-event handlers, which can interfere with this directive
|
||||
if (!this.#initialized) {
|
||||
element.addEventListener("dragstart", event => event.preventDefault());
|
||||
element.addEventListener("drag", event => event.preventDefault());
|
||||
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
// Return early if there are no changes in the passed props
|
||||
if (
|
||||
this.#lastProps.button === props.button
|
||||
&& this.#lastProps.start === props.start
|
||||
&& this.#lastProps.move === props.move
|
||||
&& this.#lastProps.end === props.end
|
||||
) {
|
||||
return noChange;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// This makes it impossible to replace the move handler on an element with
|
||||
// a different one on subsequent renders, but this may be the desired
|
||||
// behavior? This allows the move handler to be bound as a lambda that
|
||||
// will keep firing even after the DOM re-renders. See
|
||||
// studio-frontend/src/app/anim-timeline.element.ts for the use case.
|
||||
if (this.#moveHandler != null)
|
||||
return noChange;
|
||||
|
||||
this.#lastProps = { ...props };
|
||||
|
||||
// Clean up previous event handlers
|
||||
if (this.#startHandler)
|
||||
element.removeEventListener("pointerdown", this.#startHandler);
|
||||
|
||||
if (this.#moveHandler)
|
||||
// TODO: This statement is unreachable due to the early-return above
|
||||
window.removeEventListener("pointermove", this.#moveHandler);
|
||||
|
||||
// Set the pointer button which should activate dragging
|
||||
this.#pointerButton = (props.button != null) ? props.button : 0;
|
||||
|
||||
// Main event-handling behavior
|
||||
//
|
||||
// It's important to not bind any `window` events unconditionally here,
|
||||
// because those handlers could easily outlive the element this directive
|
||||
// is attached to (the "host" element), which would cause a memory leak.
|
||||
//
|
||||
// Instead, we add a single `pointerdown` listener to the host element,
|
||||
// and inside that listener, we add the `pointermove` listener to the
|
||||
// window. By removing the `pointermove` listener on `pointerup`, we
|
||||
// shouldn't have any long-lived event listeners holding (potentially
|
||||
// stale) references to the host element.
|
||||
|
||||
this.#moveHandler = (event: PointerEvent) => {
|
||||
props.move?.call(host ?? event, event);
|
||||
}
|
||||
|
||||
this.#startHandler = (event: PointerEvent) => {
|
||||
if (event.button !== this.#pointerButton)
|
||||
return;
|
||||
|
||||
window.addEventListener("pointermove", this.#moveHandler!);
|
||||
|
||||
window.addEventListener("pointerup", event => {
|
||||
props.end?.call(host ?? event, event);
|
||||
window.removeEventListener("pointermove", this.#moveHandler!);
|
||||
}, {
|
||||
once: true,
|
||||
});
|
||||
|
||||
props.start?.call(host ?? event, event);
|
||||
}
|
||||
|
||||
element.addEventListener("pointerdown", this.#startHandler);
|
||||
|
||||
return noChange;
|
||||
}
|
||||
}
|
||||
|
||||
export default directive(Drag);
|
||||
Reference in New Issue
Block a user