This commit is contained in:
Lee-Orr
2024-03-26 14:41:51 -04:00
parent 02d9acd63e
commit a03b449b6d
4 changed files with 143 additions and 33 deletions
@@ -46,6 +46,27 @@
}
}
.btn-autokey {
width: 40px;
height: 36px;
border-radius: 4px;
padding: 0;
justify-content: center;
background: #0006;
color: #FFF;
opacity: 0.5;
&:hover {
background: #90363F;
}
&.active {
background: #7A2C34E3;
opacity: 1;
}
}
.timeline-nav {
gap: 0;
margin-top: 2px;
+42 -31
View File
@@ -14,7 +14,7 @@ import styles from "./anim-editor.element.scss?inline";
import { SCENE_DATA_PROVIDER, type SceneDataProvider } from "@storyteller/studio-web/scene-data-provider";
import { TREE_SELECTION_PROVIDER, TreeSelectionProvider } from "@storyteller/studio-ui/tree";
const TRACK_ICONS : Record<string, string> = {
const TRACK_ICONS: Record<string, string> = {
camera: "video",
transform: "up-down-left-right",
light: "light"
@@ -30,10 +30,10 @@ function isOutsideEntity(
@customElement("sts-anim-editor")
@provide(TREE_SELECTION_PROVIDER)
export class AnimEditorElement extends LitElement
implements TreeSelectionProvider {
@inject(SCENE_DATA_PROVIDER)
sceneData?: SceneDataProvider;
implements TreeSelectionProvider {
@inject(SCENE_DATA_PROVIDER)
sceneData?: SceneDataProvider;
static override styles = unsafeCSS(styles);
@state() sidebarWidth = 256;
@@ -47,14 +47,16 @@ implements TreeSelectionProvider {
@state() playHead = 0;
@state() playing = false;
@state() tracks : Record<string, {name?: string, target_type: studio.SceneElement, animation_target: studio.AnimationTarget }> = {};
@state() track_ordered : [string, studio.AnimationTarget][] = [];
@state() tracks: Record<string, { name?: string, target_type: studio.SceneElement, animation_target: studio.AnimationTarget }> = {};
@state() keyframes : Record<string, Keyframe> = {};
@state() track_ordered: [string, studio.AnimationTarget][] = [];
@state() keyframes: Record<string, Keyframe> = {};
@state() selectedTrack?: { track: string } | { outsideEntity: string };
@state() keyingMode: "manual" | "automatic" = "manual";
trackCount(): number {
return Object.keys(this.tracks).length;
}
@@ -78,12 +80,12 @@ implements TreeSelectionProvider {
super.update(changes);
}
setPlayTime(time: number) : void {
setPlayTime(time: number): void {
studio.seekTime(time);
}
@on("window:seek-time-changed")
onSeekTimeChanged({ detail: {time, playing} }: SeekTimeChanged ): void {
onSeekTimeChanged({ detail: { time, playing } }: SeekTimeChanged): void {
console.log("Seek Time Changed", time, playing);
this.playHead = time;
this.playing = playing;
@@ -97,29 +99,29 @@ implements TreeSelectionProvider {
}
@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};
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).map(entity => [entity,tracks[entity].animation_target]);
this.track_ordered = Object.keys(tracks).map(entity => [entity, tracks[entity].animation_target]);
if (typeof this.selectedTrack === "object") {
if (!isOutsideEntity(this.selectedTrack) && !(this.selectedTrack.track in tracks)) {
this.selectedTrack = { outsideEntity: this.selectedTrack.track};
this.selectedTrack = { outsideEntity: this.selectedTrack.track };
} else if (isOutsideEntity(this.selectedTrack) && this.selectedTrack.outsideEntity in tracks) {
this.selectedTrack = { track: this.selectedTrack.outsideEntity};
this.selectedTrack = { track: this.selectedTrack.outsideEntity };
}
}
@@ -127,13 +129,13 @@ implements TreeSelectionProvider {
}
@on("window:update-keyframes")
onUpdateKeyframes({detail: {added, removed}}: studio.UpdateKeyframes): void {
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};
let keyframes = { ...this.keyframes };
for (let keyframe of addedKeyframes) {
keyframes[keyframe] = added[keyframe];
}
@@ -162,14 +164,20 @@ implements TreeSelectionProvider {
this.clipEnd = Math.max(max, min);
}
@on("window:keying-mode-changed")
onKeyingModeChanged({ detail: { automatic } }: studio.UpdateKeyingMode): void {
console.log("Keying Mode Changed", automatic);
this.keyingMode = automatic ? "automatic" : "manual";
}
@on("window:entity-select")
onEntitySelected({ detail: id }: EntitySelectEvent): void {
if (id) {
if (this.tracks[id]) {
this.selectedTrack = { track: id };
} else if (!this.keyframes[id]) {
this.selectedTrack = {outsideEntity: id};
this.selectedTrack = { outsideEntity: id };
} else {
this.selectedTrack = undefined;
}
@@ -206,7 +214,7 @@ implements TreeSelectionProvider {
if (!time || typeof time != "number") {
continue;
}
if (time < pos && time > closest_keyframe ) {
if (time < pos && time > closest_keyframe) {
closest_keyframe = time;
}
}
@@ -229,7 +237,7 @@ implements TreeSelectionProvider {
if (!time || typeof time != "number") {
continue;
}
if (time > pos && time < closest_keyframe ) {
if (time > pos && time < closest_keyframe) {
closest_keyframe = time;
}
}
@@ -262,8 +270,8 @@ implements TreeSelectionProvider {
protected override render(): TemplateResult {
const track_order_hashed : Record<string, number> = {};
const track_animation_target_order_hashed : Record<studio.AnimationTarget, number> = {};
const track_order_hashed: Record<string, number> = {};
const track_animation_target_order_hashed: Record<studio.AnimationTarget, number> = {};
for (const [index, [track, target]] of this.track_ordered.entries()) {
track_order_hashed[track] = index;
@@ -277,7 +285,7 @@ implements TreeSelectionProvider {
let clip_start = (this.clipStart - this.timelineStart) / length;
let clip_end = (this.clipEnd - this.timelineStart) / length;
let timeline_css_variables = {
'--track-count': `${this.trackCount()}`,
'--row-count': `calc(var(--track-count) + ${canAddSelected ? 3 : 2})`,
@@ -317,19 +325,22 @@ implements TreeSelectionProvider {
return html`
<div class="sidebar"
style=${styleMap({
width: `${this.sidebarWidth}px`,
...timeline_css_variables
})}
width: `${this.sidebarWidth}px`,
...timeline_css_variables
})}
>
<sts-toolbar>
<sts-toolbar-group>
<sts-button class="btn-keyframe"
${this.keyingMode === "automatic" ? html`<sts-button class="btn-autokey active" icon="record" aria-label="Auto Keyframe" @click=${studio.toggleAutoKey} title="Auto Keyframe"/>` :
html`<sts-button class="btn-keyframe"
icon="key"
aria-label="Add Keyframe"
title="Add Keyframe"
?disabled=${none_selected}
@click=${this.addKeyframe}
></sts-button>
<sts-button class="btn-autokey" icon="record" aria-label="Auto Keyframe" @click=${studio.toggleAutoKey} title="Auto Keyframe"/>`
}
</sts-toolbar-group>
<sts-toolbar-group class="timeline-nav">
<sts-button
@@ -345,7 +356,7 @@ implements TreeSelectionProvider {
@click=${this.prevKeyframe}
></sts-button>
<sts-button class="play"
icon=${this.playing ? "pause" : "play" }
icon=${this.playing ? "pause" : "play"}
aria-label="Play"
title="Play"
@click=${this.togglePlaying}
+2
View File
@@ -43,6 +43,7 @@ import farMountainSun from "@fortawesome/fontawesome-pro/svgs/regular/mountain-s
import farArrowRightToBracket from "@fortawesome/fontawesome-pro/svgs/regular/arrow-right-to-bracket.svg?raw";
import farMagnifyingGlass from "@fortawesome/fontawesome-pro/svgs/regular/magnifying-glass.svg?raw";
import fasHouse from "@fortawesome/fontawesome-pro/svgs/solid/house.svg?raw";
import fasRecord from "@fortawesome/fontawesome-pro/svgs/solid/circle.svg?raw";
import customCylinder from "../assets/icons/cylinder.svg?raw";
import customSphere from "../assets/icons/sphere.svg?raw";
@@ -115,6 +116,7 @@ function renderSvg(iconId: string) {
"video": () => farVideo,
"volume": () => farVolume,
"xmark": () => fasXmark,
"record": () => fasRecord,
_: () => null,
});
+78 -2
View File
@@ -33,7 +33,8 @@ impl Plugin for TimelinePlugin {
.register_type::<EaseType>()
.register_type::<uuid::Uuid>()
.add_event::<TimelineInteractionEvent>()
.add_event::<KeyframeInteractionEvent>();
.add_event::<KeyframeInteractionEvent>()
.add_state::<KeyingMode>();
app.insert_resource(GlobalSeekTime {
time: 0.,
@@ -48,6 +49,7 @@ impl Plugin for TimelinePlugin {
app.add_systems(
Update,
(
process_auto_key.run_if(in_state(KeyingMode::Auto)),
process_keyframe_events,
timeline_bound_component_animator_system::<Transform>,
),
@@ -70,6 +72,7 @@ impl Plugin for TimelinePlugin {
wasm::notify_seek_time_changes.map(bevy::utils::error),
wasm::notify_keyframe_changes.map(bevy::utils::error),
wasm::notify_updated_animation_targets.map(bevy::utils::error),
wasm::notify_keying_mode_changed.map(bevy::utils::error),
),
);
}
@@ -84,6 +87,7 @@ pub enum TimelineInteractionEvent {
Play,
Pause,
Seek(f32),
ToggleAutoKey,
}
#[allow(dead_code)]
@@ -102,6 +106,13 @@ pub struct GlobalSeekTime {
pub delta: Option<f32>,
}
#[derive(States, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum KeyingMode {
#[default]
Manual,
Auto,
}
#[derive(Clone, Debug, Copy, Default, Reflect, PartialEq, Serialize, Deserialize)]
pub enum EaseType {
#[default]
@@ -246,6 +257,8 @@ impl FromStr for AnimationTarget {
fn process_timeline_events(
mut e_timeline_event: EventReader<TimelineInteractionEvent>,
mut r_global_seek_time: ResMut<GlobalSeekTime>,
key_mode: Res<State<KeyingMode>>,
mut next_keying_mode: ResMut<NextState<KeyingMode>>,
) {
for event in e_timeline_event.read() {
match event {
@@ -256,6 +269,12 @@ fn process_timeline_events(
TimelineInteractionEvent::Seek(time) => {
r_global_seek_time.time = time.max(0.);
}
TimelineInteractionEvent::ToggleAutoKey => {
next_keying_mode.set(match key_mode.get() {
KeyingMode::Manual => KeyingMode::Auto,
KeyingMode::Auto => KeyingMode::Manual,
});
}
}
r_global_seek_time.delta = None;
@@ -344,6 +363,22 @@ fn process_keyframe_events(
}
}
fn process_auto_key(
changed: Query<Entity, (With<AnimationTarget>, Changed<Transform>)>,
r_global_seek_time: Res<GlobalSeekTime>,
mut event: EventWriter<KeyframeInteractionEvent>,
) {
if r_global_seek_time.playing || r_global_seek_time.is_changed() {
info!("Skipping Auto Key");
return;
}
for entity in &changed {
info!("Setting Auto Key");
event.send(KeyframeInteractionEvent::RecordKeyframe { target: entity })
}
}
fn generate_transform_animation(
mut commands: Commands,
keyframes: Query<
@@ -578,7 +613,7 @@ mod wasm {
};
use super::{
AnimationTarget, EaseType, GlobalSeekTime, Keyframe, KeyframeInteractionEvent,
AnimationTarget, EaseType, GlobalSeekTime, Keyframe, KeyframeInteractionEvent, KeyingMode,
TimedAnimationClipAsset, TimelineInteractionEvent, TransformKeyframe,
};
@@ -601,6 +636,11 @@ mod wasm {
SEEK_TIME.write_message(TimelineInteractionEvent::Pause);
}
#[wasm_bindgen(js_name = toggleAutoKey)]
pub fn toggle_auto_key() {
SEEK_TIME.write_message(TimelineInteractionEvent::ToggleAutoKey);
}
#[wasm_bindgen(js_name = recordKeyframe)]
pub fn record_keyframe(entities: Vec<String>) {
KEYFRAME_EVENT.write_message(
@@ -699,6 +739,13 @@ mod wasm {
removed: string[]
}
}
export interface UpdateKeyingMode extends CustomEvent {
type: "keying-mode-changed",
detail: {
automatic: boolean
}
}
declare global {
export interface GlobalEventHandlersEventMap {
@@ -722,6 +769,11 @@ mod wasm {
removed: Vec<String>,
}
#[derive(Serialize, Deserialize)]
struct UpdateKeyingMode {
automatic: bool,
}
#[derive(Serialize, Deserialize)]
struct TimedAnimationClipInfo {
entity: String,
@@ -914,4 +966,28 @@ mod wasm {
&serde_json::to_value(r_global_seek_time.as_ref()).map_err(|e| format!("{e}"))?,
)
}
pub(super) fn notify_keying_mode_changed(
r_keying_mode: Res<State<KeyingMode>>,
q_window: Query<&Window, With<PrimaryWindow>>,
) -> Result<(), String> {
if !r_keying_mode.is_changed() {
return Ok(());
}
let Ok(window) = q_window.get_single() else {
return Ok(());
};
crate::wasm::dispatch_to_js(
window,
"keying-mode-changed",
&serde_json::to_value(UpdateKeyingMode {
automatic: match r_keying_mode.get() {
KeyingMode::Manual => false,
KeyingMode::Auto => true,
},
})
.map_err(|e| format!("{e}"))?,
)
}
}