class_name ButtonBounce extends RefCounted ## Small animations that make buttons feel alive: a button grows a little under the mouse, ## shrinks while it is held down, and can pop when its icon changes. ## ## The button is scaled around its middle, so its place in a container does not change. ## With [member Settings.reduce_animations] on, buttons keep their size. ## How big a button is under the mouse. const HOVER_SCALE: Vector2 = Vector2(1.08, 1.08) ## How big a button is while it is held down. const PRESSED_SCALE: Vector2 = Vector2(0.92, 0.92) ## How small a button starts when it pops. const POP_SCALE: Vector2 = Vector2(0.8, 0.8) ## How long growing or shrinking takes, in seconds. const DURATION: float = 0.08 ## How long a pop takes, in seconds. const POP_DURATION: float = 0.18 const _TWEEN_META: StringName = &"button_bounce_tween" const _PIVOT_META: StringName = &"button_bounce_pivot" const _ADDED_META: StringName = &"button_bounce_added" ## Gives every button in [param tree] the bounce, also buttons added later, e.g. rows ## of the player list. Check buttons are left out: they span a whole settings row. static func add_to_every_button(tree: SceneTree) -> void: if !tree.node_added.is_connected(_on_node_added): tree.node_added.connect(_on_node_added) var nodes: Array[Node] = [tree.root] while !nodes.is_empty(): var node: Node = nodes.pop_back() _on_node_added(node) nodes.append_array(node.get_children()) static func _on_node_added(node: Node) -> void: if node is BaseButton && !(node is CheckButton) && !(node is CheckBox): add_to(node) ## Makes [param button] grow under the mouse and shrink while held down. Adding it to ## the same button twice does nothing. static func add_to(button: BaseButton) -> void: if button.has_meta(_ADDED_META): return button.set_meta(_ADDED_META, true) _keep_pivot_centred(button) button.mouse_entered.connect(func() -> void: if !button.disabled: _scale_to(button, HOVER_SCALE)) button.mouse_exited.connect(func() -> void: _scale_to(button, Vector2.ONE)) button.button_down.connect(func() -> void: _scale_to(button, PRESSED_SCALE)) button.button_up.connect(func() -> void: _scale_to(button, resting_scale(button))) ## The size [param button] goes back to when it is not held down: bigger under the mouse. static func resting_scale(button: BaseButton) -> Vector2: return HOVER_SCALE if button.is_hovered() && !button.disabled else Vector2.ONE ## Shrinks [param button] and lets it spring back, e.g. when its icon changes. static func pop(button: BaseButton) -> void: if !Anim.enabled(): return _keep_pivot_centred(button) button.scale = POP_SCALE var tween: Tween = _new_tween(button) tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tween.tween_property(button, ^"scale", resting_scale(button), POP_DURATION) static func _scale_to(button: BaseButton, target: Vector2) -> void: if !Anim.enabled(): button.scale = Vector2.ONE return var tween: Tween = _new_tween(button) tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) tween.tween_property(button, ^"scale", target, DURATION) # A new tween for [param button], stopping the one before so they never fight static func _new_tween(button: BaseButton) -> Tween: if button.has_meta(_TWEEN_META): var running: Tween = button.get_meta(_TWEEN_META) if running.is_valid(): running.kill() var tween: Tween = button.create_tween() button.set_meta(_TWEEN_META, tween) return tween static func _keep_pivot_centred(button: BaseButton) -> void: button.pivot_offset = button.size / 2 if !button.has_meta(_PIVOT_META): button.set_meta(_PIVOT_META, true) button.resized.connect(func() -> void: button.pivot_offset = button.size / 2)