Browse Source

Merge pull request #53298 from NathanLovato/GDQuest/animation-bezier-editor-ui-improvements

Rémi Verschelde 3 years ago
parent
commit
5045f46a5c

+ 27 - 0
doc/classes/Animation.xml

@@ -130,6 +130,14 @@
 				Sets the stream of the key identified by [code]key_idx[/code] to value [code]stream[/code]. The [code]track_idx[/code] must be the index of an Audio Track.
 			</description>
 		</method>
+		<method name="bezier_track_get_key_handle_mode" qualifiers="const">
+			<return type="int" />
+			<argument index="0" name="track_idx" type="int" />
+			<argument index="1" name="key_idx" type="int" />
+			<description>
+				Returns the handle mode of the key identified by [code]index[/code]. See [enum HandleMode] for possible values. The [code]track_idx[/code] must be the index of a Bezier Track.
+			</description>
+		</method>
 		<method name="bezier_track_get_key_in_handle" qualifiers="const">
 			<return type="Vector2" />
 			<argument index="0" name="track_idx" type="int" />
@@ -161,6 +169,7 @@
 			<argument index="2" name="value" type="float" />
 			<argument index="3" name="in_handle" type="Vector2" default="Vector2(0, 0)" />
 			<argument index="4" name="out_handle" type="Vector2" default="Vector2(0, 0)" />
+			<argument index="5" name="handle_mode" type="int" enum="Animation.HandleMode" default="1" />
 			<description>
 				Inserts a Bezier Track key at the given [code]time[/code] in seconds. The [code]track_idx[/code] must be the index of a Bezier Track.
 				[code]in_handle[/code] is the left-side weight of the added Bezier curve point, [code]out_handle[/code] is the right-side one, while [code]value[/code] is the actual value at this point.
@@ -174,11 +183,22 @@
 				Returns the interpolated value at the given [code]time[/code] (in seconds). The [code]track_idx[/code] must be the index of a Bezier Track.
 			</description>
 		</method>
+		<method name="bezier_track_set_key_handle_mode">
+			<return type="void" />
+			<argument index="0" name="track_idx" type="int" />
+			<argument index="1" name="key_idx" type="int" />
+			<argument index="2" name="key_handle_mode" type="int" enum="Animation.HandleMode" />
+			<argument index="3" name="balanced_value_time_ratio" type="float" default="1.0" />
+			<description>
+				Changes the handle mode of the keyframe at the given [code]index[/code]. See [enum HandleMode] for possible values. The [code]track_idx[/code] must be the index of a Bezier Track.
+			</description>
+		</method>
 		<method name="bezier_track_set_key_in_handle">
 			<return type="void" />
 			<argument index="0" name="track_idx" type="int" />
 			<argument index="1" name="key_idx" type="int" />
 			<argument index="2" name="in_handle" type="Vector2" />
+			<argument index="3" name="balanced_value_time_ratio" type="float" default="1.0" />
 			<description>
 				Sets the in handle of the key identified by [code]key_idx[/code] to value [code]in_handle[/code]. The [code]track_idx[/code] must be the index of a Bezier Track.
 			</description>
@@ -188,6 +208,7 @@
 			<argument index="0" name="track_idx" type="int" />
 			<argument index="1" name="key_idx" type="int" />
 			<argument index="2" name="out_handle" type="Vector2" />
+			<argument index="3" name="balanced_value_time_ratio" type="float" default="1.0" />
 			<description>
 				Sets the out handle of the key identified by [code]key_idx[/code] to value [code]out_handle[/code]. The [code]track_idx[/code] must be the index of a Bezier Track.
 			</description>
@@ -619,5 +640,11 @@
 		<constant name="LOOP_PINGPONG" value="2" enum="LoopMode">
 			Repeats playback and reverse playback at both ends of the animation.
 		</constant>
+		<constant name="HANDLE_MODE_FREE" value="0" enum="HandleMode">
+			Assigning the free handle mode to a Bezier Track's keyframe allows you to edit the keyframe's left and right handles independently from one another.
+		</constant>
+		<constant name="HANDLE_MODE_BALANCED" value="1" enum="HandleMode">
+			Assigning the balanced handle mode to a Bezier Track's keyframe makes it so the two handles of the keyframe always stay aligned when changing either the keyframe's left or right handle.
+		</constant>
 	</constants>
 </class>

+ 64 - 38
editor/animation_bezier_editor.cpp

@@ -187,7 +187,7 @@ void AnimationBezierTrackEdit::_draw_line_clipped(const Vector2 &p_from, const V
 	Vector2 from = p_from;
 	Vector2 to = p_to;
 
-	if (from.x == to.x) {
+	if (from.x == to.x && from.y == to.y) {
 		return;
 	}
 	if (to.x < from.x) {
@@ -222,11 +222,6 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 		bezier_icon = get_theme_icon(SNAME("KeyBezierPoint"), SNAME("EditorIcons"));
 		bezier_handle_icon = get_theme_icon(SNAME("KeyBezierHandle"), SNAME("EditorIcons"));
 		selected_icon = get_theme_icon(SNAME("KeyBezierSelected"), SNAME("EditorIcons"));
-		if (handle_mode_option->get_item_count() == 0) {
-			handle_mode_option->add_icon_item(get_theme_icon(SNAME("BezierHandlesFree"), SNAME("EditorIcons")), TTR("Free"), HANDLE_MODE_FREE);
-			handle_mode_option->add_icon_item(get_theme_icon(SNAME("BezierHandlesBalanced"), SNAME("EditorIcons")), TTR("Balanced"), HANDLE_MODE_BALANCED);
-			handle_mode_option->add_icon_item(get_theme_icon(SNAME("BezierHandlesMirror"), SNAME("EditorIcons")), TTR("Mirror"), HANDLE_MODE_MIRROR);
-		}
 	}
 	if (p_what == NOTIFICATION_RESIZED) {
 		int right_limit = get_size().width - timeline->get_buttons_width();
@@ -420,9 +415,9 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 
 		//draw editor handles
 		{
-			float scale = timeline->get_zoom_scale();
 			edit_points.clear();
 
+			float scale = timeline->get_zoom_scale();
 			for (int i = 0; i < animation->track_get_key_count(track); i++) {
 				float offset = animation->track_get_key_time(track, i);
 				float value = animation->bezier_track_get_key_value(track, i);
@@ -438,7 +433,7 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 				if (moving_handle != 0 && moving_handle_key == i) {
 					in_vec = moving_handle_left;
 				}
-				Vector2 pos_in = Vector2(((offset + in_vec.x) - timeline->get_value()) * scale + limit, _bezier_h_to_pixel(value + in_vec.y));
+				Vector2 pos_in(((offset + in_vec.x) - timeline->get_value()) * scale + limit, _bezier_h_to_pixel(value + in_vec.y));
 
 				Vector2 out_vec = animation->bezier_track_get_key_out_handle(track, i);
 
@@ -446,7 +441,7 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 					out_vec = moving_handle_right;
 				}
 
-				Vector2 pos_out = Vector2(((offset + out_vec.x) - timeline->get_value()) * scale + limit, _bezier_h_to_pixel(value + out_vec.y));
+				Vector2 pos_out(((offset + out_vec.x) - timeline->get_value()) * scale + limit, _bezier_h_to_pixel(value + out_vec.y));
 
 				_draw_line_clipped(pos, pos_in, accent, limit, right_limit);
 				_draw_line_clipped(pos, pos_out, accent, limit, right_limit);
@@ -581,11 +576,21 @@ void AnimationBezierTrackEdit::_clear_selection() {
 	update();
 }
 
+void AnimationBezierTrackEdit::_change_selected_keys_handle_mode(Animation::HandleMode p_mode) {
+	undo_redo->create_action(TTR("Update Selected Key Handles"));
+	double ratio = timeline->get_zoom_scale() * v_zoom;
+	for (Set<int>::Element *E = selection.back(); E; E = E->prev()) {
+		const int key_index = E->get();
+		undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_handle_mode", track, key_index, animation->bezier_track_get_key_handle_mode(track, key_index), ratio);
+		undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_handle_mode", track, key_index, p_mode, ratio);
+	}
+	undo_redo->commit_action();
+}
+
 void AnimationBezierTrackEdit::_clear_selection_for_anim(const Ref<Animation> &p_anim) {
 	if (!(animation == p_anim)) {
 		return;
 	}
-	//selection.clear();
 	_clear_selection();
 }
 
@@ -667,6 +672,9 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 				menu->add_icon_item(get_theme_icon(SNAME("Duplicate"), SNAME("EditorIcons")), TTR("Duplicate Selected Key(s)"), MENU_KEY_DUPLICATE);
 				menu->add_separator();
 				menu->add_icon_item(get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")), TTR("Delete Selected Key(s)"), MENU_KEY_DELETE);
+				menu->add_separator();
+				menu->add_icon_item(get_theme_icon(SNAME("BezierHandlesFree"), SNAME("EditorIcons")), TTR("Make Handles Free"), MENU_KEY_SET_HANDLE_FREE);
+				menu->add_icon_item(get_theme_icon(SNAME("BezierHandlesBalanced"), SNAME("EditorIcons")), TTR("Make Handles Balanced"), MENU_KEY_SET_HANDLE_BALANCED);
 			}
 
 			menu->set_as_minsize();
@@ -676,10 +684,6 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 	}
 
 	if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
-		if (close_icon_rect.has_point(mb->get_position())) {
-			emit_signal(SNAME("close_request"));
-			return;
-		}
 		for (const KeyValue<int, Rect2> &E : subtracks) {
 			if (E.value.has_point(mb->get_position())) {
 				set_animation_and_track(animation, E.key);
@@ -746,7 +750,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 		//insert new point
 		if (mb->is_command_pressed() && mb->get_position().x >= timeline->get_name_limit() && mb->get_position().x < get_size().width - timeline->get_buttons_width()) {
 			Array new_point;
-			new_point.resize(5);
+			new_point.resize(6);
 
 			float h = (get_size().height / 2 - mb->get_position().y) * v_zoom + v_scroll;
 
@@ -755,6 +759,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 			new_point[2] = 0;
 			new_point[3] = 0.25;
 			new_point[4] = 0;
+			new_point[5] = 0;
 
 			float time = ((mb->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();
 			while (animation->track_find_key(track, time, true) != -1) {
@@ -986,33 +991,49 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 
 		if (moving_handle == -1) {
 			moving_handle_left = moving_handle_value;
-			if (moving_handle_left.x > 0) {
-				moving_handle_left.x = 0;
-			}
 
-			if (handle_mode_option->get_selected() == HANDLE_MODE_BALANCED) {
-				Vector2 scale = Vector2(timeline->get_zoom_scale(), v_zoom);
-				moving_handle_right = (-(moving_handle_left * scale).normalized() * (moving_handle_right * scale).length()) / scale;
+			if (animation->bezier_track_get_key_handle_mode(track, moving_handle_key) == Animation::HANDLE_MODE_BALANCED) {
+				double ratio = timeline->get_zoom_scale() * v_zoom;
+				Transform2D xform;
+				xform.set_scale(Vector2(1.0, 1.0 / ratio));
 
-			} else if (handle_mode_option->get_selected() == HANDLE_MODE_MIRROR) {
-				moving_handle_right = -moving_handle_left;
-			}
-		}
+				Vector2 vec_out = xform.xform(moving_handle_right);
+				Vector2 vec_in = xform.xform(moving_handle_left);
 
-		if (moving_handle == 1) {
-			moving_handle_right = moving_handle_value;
-			if (moving_handle_right.x < 0) {
-				moving_handle_right.x = 0;
+				moving_handle_right = xform.affine_inverse().xform(-vec_in.normalized() * vec_out.length());
 			}
+		} else if (moving_handle == 1) {
+			moving_handle_right = moving_handle_value;
+
+			if (animation->bezier_track_get_key_handle_mode(track, moving_handle_key) == Animation::HANDLE_MODE_BALANCED) {
+				double ratio = timeline->get_zoom_scale() * v_zoom;
+				Transform2D xform;
+				xform.set_scale(Vector2(1.0, 1.0 / ratio));
 
-			if (handle_mode_option->get_selected() == HANDLE_MODE_BALANCED) {
-				Vector2 scale = Vector2(timeline->get_zoom_scale(), v_zoom);
-				moving_handle_left = (-(moving_handle_right * scale).normalized() * (moving_handle_left * scale).length()) / scale;
-			} else if (handle_mode_option->get_selected() == HANDLE_MODE_MIRROR) {
-				moving_handle_left = -moving_handle_right;
+				Vector2 vec_in = xform.xform(moving_handle_left);
+				Vector2 vec_out = xform.xform(moving_handle_right);
+
+				moving_handle_left = xform.affine_inverse().xform(-vec_out.normalized() * vec_in.length());
 			}
 		}
+		update();
+	}
+
+	bool is_finishing_key_handle_drag = moving_handle != 0 && mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT;
+	if (is_finishing_key_handle_drag) {
+		undo_redo->create_action(TTR("Move Bezier Points"));
+		if (moving_handle == -1) {
+			double ratio = timeline->get_zoom_scale() * v_zoom;
+			undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_in_handle", track, moving_handle_key, moving_handle_left, ratio);
+			undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_in_handle", track, moving_handle_key, animation->bezier_track_get_key_in_handle(track, moving_handle_key), ratio);
+		} else if (moving_handle == 1) {
+			double ratio = timeline->get_zoom_scale() * v_zoom;
+			undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, moving_handle_key, moving_handle_right, ratio);
+			undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, moving_handle_key, animation->bezier_track_get_key_out_handle(track, moving_handle_key), ratio);
+		}
+		undo_redo->commit_action();
 
+		moving_handle = 0;
 		update();
 	}
 }
@@ -1021,7 +1042,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) {
 	switch (p_index) {
 		case MENU_KEY_INSERT: {
 			Array new_point;
-			new_point.resize(5);
+			new_point.resize(6);
 
 			float h = (get_size().height / 2 - menu_insert_key.y) * v_zoom + v_scroll;
 
@@ -1030,6 +1051,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) {
 			new_point[2] = 0;
 			new_point[3] = 0.25;
 			new_point[4] = 0;
+			new_point[5] = Animation::HANDLE_MODE_BALANCED;
 
 			float time = ((menu_insert_key.x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();
 			while (animation->track_find_key(track, time, true) != -1) {
@@ -1048,6 +1070,12 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) {
 		case MENU_KEY_DELETE: {
 			delete_selection();
 		} break;
+		case MENU_KEY_SET_HANDLE_FREE: {
+			_change_selected_keys_handle_mode(Animation::HANDLE_MODE_FREE);
+		} break;
+		case MENU_KEY_SET_HANDLE_BALANCED: {
+			_change_selected_keys_handle_mode(Animation::HANDLE_MODE_BALANCED);
+		} break;
 	}
 }
 
@@ -1118,6 +1146,7 @@ void AnimationBezierTrackEdit::delete_selection() {
 		undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);
 		undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);
 		undo_redo->commit_action();
+
 		//selection.clear();
 	}
 }
@@ -1150,8 +1179,6 @@ AnimationBezierTrackEdit::AnimationBezierTrackEdit() {
 	set_focus_mode(FOCUS_CLICK);
 
 	set_clip_contents(true);
-	handle_mode = HANDLE_MODE_FREE;
-	handle_mode_option = memnew(OptionButton);
 
 	close_button = memnew(Button);
 	close_button->connect("pressed", Callable(this, SNAME("emit_signal")), varray(SNAME("close_request")));
@@ -1160,7 +1187,6 @@ AnimationBezierTrackEdit::AnimationBezierTrackEdit() {
 	right_column = memnew(VBoxContainer);
 	right_column->add_child(close_button);
 	right_column->add_spacer();
-	right_column->add_child(handle_mode_option);
 	add_child(right_column);
 
 	menu = memnew(PopupMenu);

+ 5 - 12
editor/animation_bezier_editor.h

@@ -36,21 +36,14 @@
 class AnimationBezierTrackEdit : public Control {
 	GDCLASS(AnimationBezierTrackEdit, Control);
 
-	enum HandleMode {
-		HANDLE_MODE_FREE,
-		HANDLE_MODE_BALANCED,
-		HANDLE_MODE_MIRROR
-	};
-
 	enum {
 		MENU_KEY_INSERT,
 		MENU_KEY_DUPLICATE,
-		MENU_KEY_DELETE
+		MENU_KEY_DELETE,
+		MENU_KEY_SET_HANDLE_FREE,
+		MENU_KEY_SET_HANDLE_BALANCED,
 	};
 
-	HandleMode handle_mode;
-	OptionButton *handle_mode_option;
-
 	VBoxContainer *right_column;
 	Button *close_button;
 
@@ -69,8 +62,6 @@ class AnimationBezierTrackEdit : public Control {
 	Ref<Texture2D> bezier_handle_icon;
 	Ref<Texture2D> selected_icon;
 
-	Rect2 close_icon_rect;
-
 	Map<int, Rect2> subtracks;
 
 	float v_scroll = 0;
@@ -104,10 +95,12 @@ class AnimationBezierTrackEdit : public Control {
 	int moving_handle_key = 0;
 	Vector2 moving_handle_left;
 	Vector2 moving_handle_right;
+	int moving_handle_mode; // value from Animation::HandleMode
 
 	void _clear_selection();
 	void _clear_selection_for_anim(const Ref<Animation> &p_anim);
 	void _select_at_anim(const Ref<Animation> &p_anim, int p_track, float p_pos);
+	void _change_selected_keys_handle_mode(Animation::HandleMode p_mode);
 
 	Vector2 menu_insert_key;
 

+ 52 - 1
editor/animation_track_editor.cpp

@@ -334,6 +334,22 @@ public:
 					setting = false;
 					return true;
 				}
+
+				if (name == "handle_mode") {
+					const Variant &value = p_value;
+
+					setting = true;
+					undo_redo->create_action(TTR("Anim Change Keyframe Value"), UndoRedo::MERGE_ENDS);
+					int prev = animation->bezier_track_get_key_handle_mode(track, key);
+					undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_handle_mode", track, key, value);
+					undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_handle_mode", track, key, prev);
+					undo_redo->add_do_method(this, "_update_obj", animation);
+					undo_redo->add_undo_method(this, "_update_obj", animation);
+					undo_redo->commit_action();
+
+					setting = false;
+					return true;
+				}
 			} break;
 			case Animation::TYPE_AUDIO: {
 				if (name == "stream") {
@@ -498,6 +514,11 @@ public:
 					return true;
 				}
 
+				if (name == "handle_mode") {
+					r_ret = animation->bezier_track_get_key_handle_mode(track, key);
+					return true;
+				}
+
 			} break;
 			case Animation::TYPE_AUDIO: {
 				if (name == "stream") {
@@ -610,6 +631,7 @@ public:
 				p_list->push_back(PropertyInfo(Variant::FLOAT, "value"));
 				p_list->push_back(PropertyInfo(Variant::VECTOR2, "in_handle"));
 				p_list->push_back(PropertyInfo(Variant::VECTOR2, "out_handle"));
+				p_list->push_back(PropertyInfo(Variant::INT, "handle_mode", PROPERTY_HINT_ENUM, "Free,Balanced"));
 
 			} break;
 			case Animation::TYPE_AUDIO: {
@@ -949,6 +971,17 @@ public:
 							undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, value);
 							undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_out_handle", track, key, prev);
 							update_obj = true;
+						} else if (name == "handle_mode") {
+							const Variant &value = p_value;
+
+							if (!setting) {
+								setting = true;
+								undo_redo->create_action(TTR("Anim Multi Change Keyframe Value"), UndoRedo::MERGE_ENDS);
+							}
+							int prev = animation->bezier_track_get_key_handle_mode(track, key);
+							undo_redo->add_do_method(animation.ptr(), "bezier_track_set_key_handle_mode", track, key, value);
+							undo_redo->add_undo_method(animation.ptr(), "bezier_track_set_key_handle_mode", track, key, prev);
+							update_obj = true;
 						}
 					} break;
 					case Animation::TYPE_AUDIO: {
@@ -1120,6 +1153,11 @@ public:
 							return true;
 						}
 
+						if (name == "handle_mode") {
+							r_ret = animation->bezier_track_get_key_handle_mode(track, key);
+							return true;
+						}
+
 					} break;
 					case Animation::TYPE_AUDIO: {
 						if (name == "stream") {
@@ -1273,6 +1311,7 @@ public:
 					p_list->push_back(PropertyInfo(Variant::FLOAT, "value"));
 					p_list->push_back(PropertyInfo(Variant::VECTOR2, "in_handle"));
 					p_list->push_back(PropertyInfo(Variant::VECTOR2, "out_handle"));
+					p_list->push_back(PropertyInfo(Variant::INT, "handle_mode", PROPERTY_HINT_ENUM, "Free,Balanced"));
 				} break;
 				case Animation::TYPE_AUDIO: {
 					p_list->push_back(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "AudioStream"));
@@ -2607,6 +2646,17 @@ String AnimationTrackEdit::get_tooltip(const Point2 &p_pos) const {
 					text += "In-Handle: " + ih + "\n";
 					Vector2 oh = animation->bezier_track_get_key_out_handle(track, key_idx);
 					text += "Out-Handle: " + oh + "\n";
+					int hm = animation->bezier_track_get_key_handle_mode(track, key_idx);
+					text += "Handle mode: ";
+					switch (hm) {
+						case Animation::HANDLE_MODE_FREE: {
+							text += "Free";
+						} break;
+						case Animation::HANDLE_MODE_BALANCED: {
+							text += "Balanced";
+						} break;
+					}
+					text += "\n";
 				} break;
 				case Animation::TYPE_AUDIO: {
 					String stream_name = "null";
@@ -4796,12 +4846,13 @@ void AnimationTrackEditor::_insert_key_from_track(float p_ofs, int p_track) {
 			Variant value;
 			_find_hint_for_track(p_track, bp, &value);
 			Array arr;
-			arr.resize(5);
+			arr.resize(6);
 			arr[0] = value;
 			arr[1] = -0.25;
 			arr[2] = 0;
 			arr[3] = 0.25;
 			arr[4] = 0;
+			arr[5] = 0;
 
 			undo_redo->create_action(TTR("Add Track Key"));
 			undo_redo->add_do_method(animation.ptr(), "track_insert_key", p_track, p_ofs, arr);

+ 99 - 27
scene/resources/animation.cpp

@@ -317,7 +317,7 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) {
 				Vector<real_t> times = d["times"];
 				Vector<real_t> values = d["points"];
 
-				ERR_FAIL_COND_V(times.size() * 5 != values.size(), false);
+				ERR_FAIL_COND_V(times.size() * 6 != values.size(), false);
 
 				if (times.size()) {
 					int valcount = times.size();
@@ -330,11 +330,12 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) {
 					for (int i = 0; i < valcount; i++) {
 						bt->values.write[i].time = rt[i];
 						bt->values.write[i].transition = 0; //unused in bezier
-						bt->values.write[i].value.value = rv[i * 5 + 0];
-						bt->values.write[i].value.in_handle.x = rv[i * 5 + 1];
-						bt->values.write[i].value.in_handle.y = rv[i * 5 + 2];
-						bt->values.write[i].value.out_handle.x = rv[i * 5 + 3];
-						bt->values.write[i].value.out_handle.y = rv[i * 5 + 4];
+						bt->values.write[i].value.value = rv[i * 6 + 0];
+						bt->values.write[i].value.in_handle.x = rv[i * 6 + 1];
+						bt->values.write[i].value.in_handle.y = rv[i * 6 + 2];
+						bt->values.write[i].value.out_handle.x = rv[i * 6 + 3];
+						bt->values.write[i].value.out_handle.y = rv[i * 6 + 4];
+						bt->values.write[i].value.handle_mode = (HandleMode)rv[i * 6 + 5];
 					}
 				}
 
@@ -698,7 +699,7 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const {
 				int kk = bt->values.size();
 
 				key_times.resize(kk);
-				key_points.resize(kk * 5);
+				key_points.resize(kk * 6);
 
 				real_t *wti = key_times.ptrw();
 				real_t *wpo = key_points.ptrw();
@@ -709,11 +710,12 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const {
 
 				for (int i = 0; i < kk; i++) {
 					wti[idx] = vls[i].time;
-					wpo[idx * 5 + 0] = vls[i].value.value;
-					wpo[idx * 5 + 1] = vls[i].value.in_handle.x;
-					wpo[idx * 5 + 2] = vls[i].value.in_handle.y;
-					wpo[idx * 5 + 3] = vls[i].value.out_handle.x;
-					wpo[idx * 5 + 4] = vls[i].value.out_handle.y;
+					wpo[idx * 6 + 0] = vls[i].value.value;
+					wpo[idx * 6 + 1] = vls[i].value.in_handle.x;
+					wpo[idx * 6 + 2] = vls[i].value.in_handle.y;
+					wpo[idx * 6 + 3] = vls[i].value.out_handle.x;
+					wpo[idx * 6 + 4] = vls[i].value.out_handle.y;
+					wpo[idx * 6 + 5] = (double)vls[i].value.handle_mode;
 					idx++;
 				}
 
@@ -1623,7 +1625,7 @@ void Animation::track_insert_key(int p_track, double p_time, const Variant &p_ke
 			BezierTrack *bt = static_cast<BezierTrack *>(t);
 
 			Array arr = p_key;
-			ERR_FAIL_COND(arr.size() != 5);
+			ERR_FAIL_COND(arr.size() != 6);
 
 			TKey<BezierKey> k;
 			k.time = p_time;
@@ -1632,6 +1634,7 @@ void Animation::track_insert_key(int p_track, double p_time, const Variant &p_ke
 			k.value.in_handle.y = arr[2];
 			k.value.out_handle.x = arr[3];
 			k.value.out_handle.y = arr[4];
+			k.value.handle_mode = (HandleMode) int(arr[5]);
 			_insert(p_time, bt->values, k);
 
 		} break;
@@ -1770,12 +1773,13 @@ Variant Animation::track_get_key_value(int p_track, int p_key_idx) const {
 			ERR_FAIL_INDEX_V(p_key_idx, bt->values.size(), Variant());
 
 			Array arr;
-			arr.resize(5);
+			arr.resize(6);
 			arr[0] = bt->values[p_key_idx].value.value;
 			arr[1] = bt->values[p_key_idx].value.in_handle.x;
 			arr[2] = bt->values[p_key_idx].value.in_handle.y;
 			arr[3] = bt->values[p_key_idx].value.out_handle.x;
 			arr[4] = bt->values[p_key_idx].value.out_handle.y;
+			arr[5] = (double)bt->values[p_key_idx].value.handle_mode;
 			return arr;
 
 		} break;
@@ -2144,13 +2148,14 @@ void Animation::track_set_key_value(int p_track, int p_key_idx, const Variant &p
 			ERR_FAIL_INDEX(p_key_idx, bt->values.size());
 
 			Array arr = p_value;
-			ERR_FAIL_COND(arr.size() != 5);
+			ERR_FAIL_COND(arr.size() != 6);
 
 			bt->values.write[p_key_idx].value.value = arr[0];
 			bt->values.write[p_key_idx].value.in_handle.x = arr[1];
 			bt->values.write[p_key_idx].value.in_handle.y = arr[2];
 			bt->values.write[p_key_idx].value.out_handle.x = arr[3];
 			bt->values.write[p_key_idx].value.out_handle.y = arr[4];
+			bt->values.write[p_key_idx].value.handle_mode = (HandleMode) int(arr[5]);
 
 		} break;
 		case TYPE_AUDIO: {
@@ -3203,7 +3208,7 @@ StringName Animation::method_track_get_name(int p_track, int p_key_idx) const {
 	return pm->methods[p_key_idx].method;
 }
 
-int Animation::bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle) {
+int Animation::bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const HandleMode p_handle_mode) {
 	ERR_FAIL_INDEX_V(p_track, tracks.size(), -1);
 	Track *t = tracks[p_track];
 	ERR_FAIL_COND_V(t->type != TYPE_BEZIER, -1);
@@ -3221,6 +3226,7 @@ int Animation::bezier_track_insert_key(int p_track, double p_time, real_t p_valu
 	if (k.value.out_handle.x < 0) {
 		k.value.out_handle.x = 0;
 	}
+	k.value.handle_mode = p_handle_mode;
 
 	int key = _insert(p_time, bt->values, k);
 
@@ -3229,6 +3235,30 @@ int Animation::bezier_track_insert_key(int p_track, double p_time, real_t p_valu
 	return key;
 }
 
+void Animation::bezier_track_set_key_handle_mode(int p_track, int p_index, HandleMode p_mode, double p_balanced_value_time_ratio) {
+	ERR_FAIL_INDEX(p_track, tracks.size());
+	Track *t = tracks[p_track];
+	ERR_FAIL_COND(t->type != TYPE_BEZIER);
+
+	BezierTrack *bt = static_cast<BezierTrack *>(t);
+
+	ERR_FAIL_INDEX(p_index, bt->values.size());
+
+	bt->values.write[p_index].value.handle_mode = p_mode;
+
+	if (p_mode == HANDLE_MODE_BALANCED) {
+		Transform2D xform;
+		xform.set_scale(Vector2(1.0, 1.0 / p_balanced_value_time_ratio));
+
+		Vector2 vec_in = xform.xform(bt->values[p_index].value.in_handle);
+		Vector2 vec_out = xform.xform(bt->values[p_index].value.out_handle);
+
+		bt->values.write[p_index].value.in_handle = xform.affine_inverse().xform(-vec_out.normalized() * vec_in.length());
+	}
+
+	emit_changed();
+}
+
 void Animation::bezier_track_set_key_value(int p_track, int p_index, real_t p_value) {
 	ERR_FAIL_INDEX(p_track, tracks.size());
 	Track *t = tracks[p_track];
@@ -3242,7 +3272,7 @@ void Animation::bezier_track_set_key_value(int p_track, int p_index, real_t p_va
 	emit_changed();
 }
 
-void Animation::bezier_track_set_key_in_handle(int p_track, int p_index, const Vector2 &p_handle) {
+void Animation::bezier_track_set_key_in_handle(int p_track, int p_index, const Vector2 &p_handle, double p_balanced_value_time_ratio) {
 	ERR_FAIL_INDEX(p_track, tracks.size());
 	Track *t = tracks[p_track];
 	ERR_FAIL_COND(t->type != TYPE_BEZIER);
@@ -3251,14 +3281,26 @@ void Animation::bezier_track_set_key_in_handle(int p_track, int p_index, const V
 
 	ERR_FAIL_INDEX(p_index, bt->values.size());
 
-	bt->values.write[p_index].value.in_handle = p_handle;
-	if (bt->values[p_index].value.in_handle.x > 0) {
-		bt->values.write[p_index].value.in_handle.x = 0;
+	Vector2 in_handle = p_handle;
+	if (in_handle.x > 0) {
+		in_handle.x = 0;
+	}
+	bt->values.write[p_index].value.in_handle = in_handle;
+
+	if (bt->values[p_index].value.handle_mode == HANDLE_MODE_BALANCED) {
+		Transform2D xform;
+		xform.set_scale(Vector2(1.0, 1.0 / p_balanced_value_time_ratio));
+
+		Vector2 vec_out = xform.xform(bt->values[p_index].value.out_handle);
+		Vector2 vec_in = xform.xform(in_handle);
+
+		bt->values.write[p_index].value.out_handle = xform.affine_inverse().xform(-vec_in.normalized() * vec_out.length());
 	}
+
 	emit_changed();
 }
 
-void Animation::bezier_track_set_key_out_handle(int p_track, int p_index, const Vector2 &p_handle) {
+void Animation::bezier_track_set_key_out_handle(int p_track, int p_index, const Vector2 &p_handle, double p_balanced_value_time_ratio) {
 	ERR_FAIL_INDEX(p_track, tracks.size());
 	Track *t = tracks[p_track];
 	ERR_FAIL_COND(t->type != TYPE_BEZIER);
@@ -3267,10 +3309,22 @@ void Animation::bezier_track_set_key_out_handle(int p_track, int p_index, const
 
 	ERR_FAIL_INDEX(p_index, bt->values.size());
 
-	bt->values.write[p_index].value.out_handle = p_handle;
-	if (bt->values[p_index].value.out_handle.x < 0) {
-		bt->values.write[p_index].value.out_handle.x = 0;
+	Vector2 out_handle = p_handle;
+	if (out_handle.x < 0) {
+		out_handle.x = 0;
+	}
+	bt->values.write[p_index].value.out_handle = out_handle;
+
+	if (bt->values[p_index].value.handle_mode == HANDLE_MODE_BALANCED) {
+		Transform2D xform;
+		xform.set_scale(Vector2(1.0, 1.0 / p_balanced_value_time_ratio));
+
+		Vector2 vec_in = xform.xform(bt->values[p_index].value.in_handle);
+		Vector2 vec_out = xform.xform(out_handle);
+
+		bt->values.write[p_index].value.in_handle = xform.affine_inverse().xform(-vec_out.normalized() * vec_in.length());
 	}
+
 	emit_changed();
 }
 
@@ -3286,6 +3340,18 @@ real_t Animation::bezier_track_get_key_value(int p_track, int p_index) const {
 	return bt->values[p_index].value.value;
 }
 
+int Animation::bezier_track_get_key_handle_mode(int p_track, int p_index) const {
+	ERR_FAIL_INDEX_V(p_track, tracks.size(), 0);
+	Track *t = tracks[p_track];
+	ERR_FAIL_COND_V(t->type != TYPE_BEZIER, 0);
+
+	BezierTrack *bt = static_cast<BezierTrack *>(t);
+
+	ERR_FAIL_INDEX_V(p_index, bt->values.size(), 0);
+
+	return bt->values[p_index].value.handle_mode;
+}
+
 Vector2 Animation::bezier_track_get_key_in_handle(int p_track, int p_index) const {
 	ERR_FAIL_INDEX_V(p_track, tracks.size(), Vector2());
 	Track *t = tracks[p_track];
@@ -3718,11 +3784,11 @@ void Animation::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("method_track_get_name", "track_idx", "key_idx"), &Animation::method_track_get_name);
 	ClassDB::bind_method(D_METHOD("method_track_get_params", "track_idx", "key_idx"), &Animation::method_track_get_params);
 
-	ClassDB::bind_method(D_METHOD("bezier_track_insert_key", "track_idx", "time", "value", "in_handle", "out_handle"), &Animation::bezier_track_insert_key, DEFVAL(Vector2()), DEFVAL(Vector2()));
+	ClassDB::bind_method(D_METHOD("bezier_track_insert_key", "track_idx", "time", "value", "in_handle", "out_handle", "handle_mode"), &Animation::bezier_track_insert_key, DEFVAL(Vector2()), DEFVAL(Vector2()), DEFVAL(Animation::HandleMode::HANDLE_MODE_BALANCED));
 
 	ClassDB::bind_method(D_METHOD("bezier_track_set_key_value", "track_idx", "key_idx", "value"), &Animation::bezier_track_set_key_value);
-	ClassDB::bind_method(D_METHOD("bezier_track_set_key_in_handle", "track_idx", "key_idx", "in_handle"), &Animation::bezier_track_set_key_in_handle);
-	ClassDB::bind_method(D_METHOD("bezier_track_set_key_out_handle", "track_idx", "key_idx", "out_handle"), &Animation::bezier_track_set_key_out_handle);
+	ClassDB::bind_method(D_METHOD("bezier_track_set_key_in_handle", "track_idx", "key_idx", "in_handle", "balanced_value_time_ratio"), &Animation::bezier_track_set_key_in_handle, DEFVAL(1.0));
+	ClassDB::bind_method(D_METHOD("bezier_track_set_key_out_handle", "track_idx", "key_idx", "out_handle", "balanced_value_time_ratio"), &Animation::bezier_track_set_key_out_handle, DEFVAL(1.0));
 
 	ClassDB::bind_method(D_METHOD("bezier_track_get_key_value", "track_idx", "key_idx"), &Animation::bezier_track_get_key_value);
 	ClassDB::bind_method(D_METHOD("bezier_track_get_key_in_handle", "track_idx", "key_idx"), &Animation::bezier_track_get_key_in_handle);
@@ -3738,6 +3804,9 @@ void Animation::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("audio_track_get_key_start_offset", "track_idx", "key_idx"), &Animation::audio_track_get_key_start_offset);
 	ClassDB::bind_method(D_METHOD("audio_track_get_key_end_offset", "track_idx", "key_idx"), &Animation::audio_track_get_key_end_offset);
 
+	ClassDB::bind_method(D_METHOD("bezier_track_set_key_handle_mode", "track_idx", "key_idx", "key_handle_mode", "balanced_value_time_ratio"), &Animation::bezier_track_set_key_handle_mode, DEFVAL(1.0));
+	ClassDB::bind_method(D_METHOD("bezier_track_get_key_handle_mode", "track_idx", "key_idx"), &Animation::bezier_track_get_key_handle_mode);
+
 	ClassDB::bind_method(D_METHOD("animation_track_insert_key", "track_idx", "time", "animation"), &Animation::animation_track_insert_key);
 	ClassDB::bind_method(D_METHOD("animation_track_set_key_animation", "track_idx", "key_idx", "animation"), &Animation::animation_track_set_key_animation);
 	ClassDB::bind_method(D_METHOD("animation_track_get_key_animation", "track_idx", "key_idx"), &Animation::animation_track_get_key_animation);
@@ -3784,6 +3853,9 @@ void Animation::_bind_methods() {
 	BIND_ENUM_CONSTANT(LOOP_NONE);
 	BIND_ENUM_CONSTANT(LOOP_LINEAR);
 	BIND_ENUM_CONSTANT(LOOP_PINGPONG);
+
+	BIND_ENUM_CONSTANT(HANDLE_MODE_FREE);
+	BIND_ENUM_CONSTANT(HANDLE_MODE_BALANCED);
 }
 
 void Animation::clear() {

+ 12 - 4
scene/resources/animation.h

@@ -72,6 +72,11 @@ public:
 		LOOP_PINGPONG,
 	};
 
+	enum HandleMode {
+		HANDLE_MODE_FREE,
+		HANDLE_MODE_BALANCED,
+	};
+
 private:
 	struct Track {
 		TrackType type = TrackType::TYPE_ANIMATION;
@@ -157,10 +162,10 @@ private:
 	};
 
 	/* BEZIER TRACK */
-
 	struct BezierKey {
 		Vector2 in_handle; //relative (x always <0)
 		Vector2 out_handle; //relative (x always >0)
+		HandleMode handle_mode = HANDLE_MODE_BALANCED;
 		real_t value = 0.0;
 	};
 
@@ -419,11 +424,13 @@ public:
 	void track_set_interpolation_type(int p_track, InterpolationType p_interp);
 	InterpolationType track_get_interpolation_type(int p_track) const;
 
-	int bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle);
+	int bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const HandleMode p_handle_mode = HandleMode::HANDLE_MODE_BALANCED);
+	void bezier_track_set_key_handle_mode(int p_track, int p_index, HandleMode p_mode, double p_balanced_value_time_ratio = 1.0);
 	void bezier_track_set_key_value(int p_track, int p_index, real_t p_value);
-	void bezier_track_set_key_in_handle(int p_track, int p_index, const Vector2 &p_handle);
-	void bezier_track_set_key_out_handle(int p_track, int p_index, const Vector2 &p_handle);
+	void bezier_track_set_key_in_handle(int p_track, int p_index, const Vector2 &p_handle, double p_balanced_value_time_ratio = 1.0);
+	void bezier_track_set_key_out_handle(int p_track, int p_index, const Vector2 &p_handle, double p_balanced_value_time_ratio = 1.0);
 	real_t bezier_track_get_key_value(int p_track, int p_index) const;
+	int bezier_track_get_key_handle_mode(int p_track, int p_index) const;
 	Vector2 bezier_track_get_key_in_handle(int p_track, int p_index) const;
 	Vector2 bezier_track_get_key_out_handle(int p_track, int p_index) const;
 
@@ -478,6 +485,7 @@ public:
 VARIANT_ENUM_CAST(Animation::TrackType);
 VARIANT_ENUM_CAST(Animation::InterpolationType);
 VARIANT_ENUM_CAST(Animation::UpdateMode);
+VARIANT_ENUM_CAST(Animation::HandleMode);
 VARIANT_ENUM_CAST(Animation::LoopMode);
 
 #endif