Popups fade in while growing and close the reverse way, with the backdrop fading along. The winner gets confetti, bouncing pictures and a score graph that draws itself from the left. Filtered statistics cards and search rows fade in, the tabs slide in from their side, and every button bounces on hover and click (ButtonBounce, now added to all buttons). In the player list the score pops on every point, the first point flashes the row, and a player on match point glows with a "Match point!" tag. A song starting fades in the title, flashes its row (kept across list rebuilds) and pops the play button. Common/anim.gd holds the helpers. A saved "Reduce animations" switch in Settings turns all movement off; the tests use it. The flash colours are theme colours (Palette first_point, match_point, flash) in every theme. Closes #48 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
3.6 KiB
GDScript
98 lines
3.6 KiB
GDScript
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)
|