From 7c7435d2cc7ef19bb42ea97c546e3fc5869593b0 Mon Sep 17 00:00:00 2001 From: Lee-Orr Date: Thu, 14 Mar 2024 18:48:03 -0400 Subject: [PATCH] move & update keyframes --- .../src/app/anim-editor.element.scss | 23 +- .../src/app/anim-editor.element.ts | 75 ++- .../src/app/anim-timeline.element.scss | 21 +- .../src/app/anim-timeline.element.ts | 40 +- studio/src/anim/timeline_animation.rs | 468 +++++++++++++++--- studio/src/scene.rs | 9 +- 6 files changed, 502 insertions(+), 134 deletions(-) diff --git a/studio-frontend/src/app/anim-editor.element.scss b/studio-frontend/src/app/anim-editor.element.scss index ac381ff..ba3b262 100644 --- a/studio-frontend/src/app/anim-editor.element.scss +++ b/studio-frontend/src/app/anim-editor.element.scss @@ -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; + } + } + } } \ No newline at end of file diff --git a/studio-frontend/src/app/anim-editor.element.ts b/studio-frontend/src/app/anim-editor.element.ts index 09cd847..5f8197d 100644 --- a/studio-frontend/src/app/anim-editor.element.ts +++ b/studio-frontend/src/app/anim-editor.element.ts @@ -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 = {}; + @state() keyframes : Record = {}; - @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 = {}; + + 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` + { + studio.select(entity); + }} + > + `; + }); + + const none_selected = !this.selected_track; + return html` @@ -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} > ` }; diff --git a/studio-frontend/src/app/anim-timeline.element.scss b/studio-frontend/src/app/anim-timeline.element.scss index fa1b681..78d0e10 100644 --- a/studio-frontend/src/app/anim-timeline.element.scss +++ b/studio-frontend/src/app/anim-timeline.element.scss @@ -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: ""; diff --git a/studio-frontend/src/app/anim-timeline.element.ts b/studio-frontend/src/app/anim-timeline.element.ts index 45ac3bc..91d1b43 100644 --- a/studio-frontend/src/app/anim-timeline.element.ts +++ b/studio-frontend/src/app/anim-timeline.element.ts @@ -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 = {}; + @property({ attribute: false }) keyframes: Record = {}; + @property({ attribute: false }) track_ordered: Record = {} @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" > -
${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`
this.selectKeyframe(id)} >
- ` + `; + } )} `; diff --git a/studio/src/anim/timeline_animation.rs b/studio/src/anim/timeline_animation.rs index a848168..4de1262 100644 --- a/studio/src/anim/timeline_animation.rs +++ b/studio/src/anim/timeline_animation.rs @@ -5,37 +5,53 @@ use bevy::{ }, prelude::*, }; + use serde::{Deserialize, Serialize}; use space_editor::space_prefab::editor_registry::EditorRegistryExt; +use crate::interaction::Selectable; + pub struct TimelinePlugin; impl Plugin for TimelinePlugin { fn build(&self, app: &mut App) { - app.register_type::() - .editor_registry::() + app.register_type::() + .editor_registry::() .register_type::() .editor_registry::() - .add_event::(); + .register_type::() + .editor_registry::() + .register_type::() + .editor_registry::() + .register_type::() + .editor_registry::() + .add_event::() + .add_event::(); app.insert_resource(GlobalSeekTime { time: 0., playing: false, }); - app.add_systems(PreUpdate, - (process_timeline_events, update_tick).chain(), - ); + app.add_systems(PreUpdate, (process_timeline_events, update_tick).chain()); + + app.add_systems(Update, process_keyframe_events); #[cfg(feature = "wasm")] { - app.add_systems(First, - wasm::update_global_seek_time); app.add_systems( - Update, + First, + ( + wasm::update_global_seek_time, + wasm::update_keyframe_interaction, + ), + ); + app.add_systems( + PostUpdate, ( wasm::notify_seek_time_changes.map(bevy::utils::error), - wasm::notify_updated_animation_targets.map(bevy::utils::error) + wasm::notify_keyframe_changes.map(bevy::utils::error), + wasm::notify_updated_animation_targets.map(bevy::utils::error), ), ); } @@ -43,12 +59,18 @@ impl Plugin for TimelinePlugin { } #[derive(Event, Clone, Copy, Debug)] -pub enum TimelineEvent { +pub enum TimelineInteractionEvent { Play, Pause, Seek(f32), } +#[derive(Event, Clone, Copy, Debug)] +pub enum KeyframeInteractionEvent { + RecordKeyframe { target: Entity }, + MoveKeyframeTime { target: Entity, time: f32 }, +} + #[derive(Resource, Clone, Copy, Debug, Default, Reflect, Serialize, Deserialize)] #[reflect(Resource)] pub struct GlobalSeekTime { @@ -56,71 +78,110 @@ pub struct GlobalSeekTime { pub playing: bool, } -#[derive(Component, Clone, Debug, Reflect, PartialEq)] -#[reflect(Component, MapEntities)] -pub struct TimedAnimationClip { - clip: Handle, - start_time: f32, - duration: f32, - incoming_transition_duration: f32, - speed: f32, - target: Entity, +#[derive(Clone, Debug, Copy, Default, Reflect, PartialEq, Serialize, Deserialize)] +pub enum EaseType { + #[default] + Linear, + CubicHermite, } -impl PartialOrd for TimedAnimationClip { - fn partial_cmp(&self, other: &Self) -> Option { - match self.start_time.partial_cmp(&other.start_time) { - Some(core::cmp::Ordering::Equal) => {} - ord => return ord, +#[derive(Component, Clone, Debug, Reflect, PartialEq)] +#[reflect(Component)] +pub struct TransformKeyframe { + incoming_easing: EaseType, + affect_translation: bool, + affect_rotation: bool, + affect_scale: bool, +} + +impl Default for TransformKeyframe { + fn default() -> Self { + Self { + incoming_easing: Default::default(), + affect_translation: true, + affect_rotation: true, + affect_scale: true, } + } +} + +#[derive(Component, Clone, Debug, Reflect, PartialEq)] +#[reflect(Component, MapEntities)] +pub struct Keyframe { + target: Entity, + time: f32, +} + +#[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, PartialOrd, Ord)] +#[reflect(Component)] +pub struct KeyframeOrder(u32); + +impl PartialOrd for Keyframe { + fn partial_cmp(&self, other: &Self) -> Option { + self.time.partial_cmp(&other.time) + } +} + +impl MapEntities for Keyframe { + fn map_entities(&mut self, entity_mapper: &mut EntityMapper) { + self.target = entity_mapper.get_or_reserve(self.target); + } +} + +impl Default for Keyframe { + fn default() -> Self { + Self { + target: Entity::PLACEHOLDER, + time: Default::default(), + } + } +} + +#[derive(Component, Clone, Debug, Reflect, PartialEq)] +#[reflect(Component)] +pub struct TimedAnimationClipAsset { + clip: Handle, + duration: f32, + speed: f32, +} + +impl PartialOrd for TimedAnimationClipAsset { + fn partial_cmp(&self, other: &Self) -> Option { match self.duration.partial_cmp(&other.duration) { Some(core::cmp::Ordering::Equal) => {} ord => return ord, } - match self.clip.partial_cmp(&other.clip) { + match self.speed.partial_cmp(&other.speed) { Some(core::cmp::Ordering::Equal) => {} ord => return ord, } - match self - .incoming_transition_duration - .partial_cmp(&other.incoming_transition_duration) - { - Some(core::cmp::Ordering::Equal) => {} - ord => return ord, - } - self.target.partial_cmp(&other.target) + None } } -impl Default for TimedAnimationClip { +impl Default for TimedAnimationClipAsset { fn default() -> Self { Self { clip: Default::default(), - start_time: Default::default(), duration: Default::default(), speed: 1.0, - incoming_transition_duration: Default::default(), - target: Entity::PLACEHOLDER, } } } -impl MapEntities for TimedAnimationClip { - fn map_entities(&mut self, entity_mapper: &mut EntityMapper) { - self.target = entity_mapper.get_or_reserve(self.target); - } -} - #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct AnimationTarget; -fn process_timeline_events(mut e_timeline_event: EventReader, mut r_global_seek_time: ResMut) { +fn process_timeline_events( + mut e_timeline_event: EventReader, + mut r_global_seek_time: ResMut, +) { for event in e_timeline_event.read() { match event { - TimelineEvent::Play => r_global_seek_time.playing = true, - TimelineEvent::Pause => r_global_seek_time.playing = false, - TimelineEvent::Seek(time) => r_global_seek_time.time = *time, + TimelineInteractionEvent::Play => r_global_seek_time.playing = true, + TimelineInteractionEvent::Pause => r_global_seek_time.playing = false, + TimelineInteractionEvent::Seek(time) => r_global_seek_time.time = *time, } } } @@ -131,39 +192,116 @@ fn update_tick(mut r_global_seek_time: ResMut, time: Res