Files
MusicPlayer/common/button_bounce.gd
SansanandClaude Opus 5.5 0fc3be79e1
Build / test (push) Successful in 1m39s
Build / build (push) Successful in 1m14s
Add animations to popups, points, songs, the winner and the tabs (#48)
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>
2026-10-12 21:49:01 +02:00

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)