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>
141 lines
5.3 KiB
GDScript
141 lines
5.3 KiB
GDScript
class_name Anim
|
|
extends RefCounted
|
|
## Short animations made with tweens, used all over the game: popping, fading, flashing
|
|
## and pulsing.
|
|
##
|
|
## Every animation is short so a quiz never waits for one. With
|
|
## [member Settings.reduce_animations] on, nothing moves: each helper jumps straight to
|
|
## where the animation would end.
|
|
|
|
## How long quick feedback, such as a button press, takes.
|
|
const FAST: float = 0.12
|
|
## How long most animations take.
|
|
const NORMAL: float = 0.2
|
|
## How long flashes and bigger moments take.
|
|
const SLOW: float = 0.45
|
|
|
|
|
|
## Whether animations are on.
|
|
static func enabled() -> bool:
|
|
return !Settings.reduce_animations
|
|
|
|
|
|
## A new tween on [param node] for the animation called [param key], stopping an earlier
|
|
## one with the same key so they don't fight. Returns null when animations are off or the
|
|
## node is not in the tree.
|
|
static func tween(node: Node, key: StringName = &"anim") -> Tween:
|
|
var meta: StringName = StringName("_tween_" + key)
|
|
var old: Variant = node.get_meta(meta) if node.has_meta(meta) else null
|
|
if old is Tween && (old as Tween).is_valid():
|
|
(old as Tween).kill()
|
|
if !enabled() || !node.is_inside_tree():
|
|
return null
|
|
var new_tween: Tween = node.create_tween()
|
|
node.set_meta(meta, new_tween)
|
|
return new_tween
|
|
|
|
|
|
## Makes [param control] grow from its middle.
|
|
static func center_pivot(control: Control) -> void:
|
|
control.pivot_offset = control.size / 2
|
|
|
|
|
|
## Makes [param control] jump to [param peak] times its size and spring back.
|
|
static func pop(control: Control, peak: float = 1.3, duration: float = NORMAL) -> void:
|
|
var pop_tween: Tween = tween(control, &"pop")
|
|
control.scale = Vector2.ONE
|
|
if pop_tween == null:
|
|
return
|
|
center_pivot(control)
|
|
control.scale = Vector2.ONE * peak
|
|
pop_tween.tween_property(control, "scale", Vector2.ONE, duration) \
|
|
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
|
|
|
|
|
## Makes [param control] grow from [param from] times its size with a bounce.
|
|
static func bounce_in(control: Control, from: float = 0.4, duration: float = SLOW) -> void:
|
|
var bounce_tween: Tween = tween(control, &"pop")
|
|
control.scale = Vector2.ONE
|
|
if bounce_tween == null:
|
|
return
|
|
center_pivot(control)
|
|
control.scale = Vector2.ONE * from
|
|
bounce_tween.tween_property(control, "scale", Vector2.ONE, duration * 1.6) \
|
|
.set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
|
|
|
|
|
|
## Fades [param item] in from invisible.
|
|
static func fade_in(item: CanvasItem, duration: float = NORMAL) -> void:
|
|
var fade_tween: Tween = tween(item, &"fade")
|
|
item.modulate.a = 1.0
|
|
if fade_tween == null:
|
|
return
|
|
item.modulate.a = 0.0
|
|
fade_tween.tween_property(item, "modulate:a", 1.0, duration)
|
|
|
|
|
|
## Slides [param control] in from [param offset] away from where it is, fading in.
|
|
static func slide_in(control: Control, offset: Vector2, duration: float = NORMAL) -> void:
|
|
var slide_tween: Tween = tween(control, &"slide")
|
|
if control.has_meta(&"_slide_home"):
|
|
control.position = control.get_meta(&"_slide_home")
|
|
control.modulate.a = 1.0
|
|
if slide_tween == null:
|
|
return
|
|
var home: Vector2 = control.position
|
|
control.set_meta(&"_slide_home", home)
|
|
control.position = home + offset
|
|
control.modulate.a = 0.0
|
|
slide_tween.set_parallel()
|
|
slide_tween.tween_property(control, "position", home, duration) \
|
|
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
slide_tween.tween_property(control, "modulate:a", 1.0, duration)
|
|
slide_tween.chain().tween_callback(func() -> void: control.remove_meta(&"_slide_home"))
|
|
|
|
|
|
## Scales [param control] to [param target] times its size, e.g. a little bigger under
|
|
## the mouse.
|
|
static func scale_to(control: Control, target: float, duration: float = FAST) -> void:
|
|
var scale_tween: Tween = tween(control, &"scale")
|
|
if scale_tween == null:
|
|
control.scale = Vector2.ONE
|
|
return
|
|
center_pivot(control)
|
|
scale_tween.tween_property(control, "scale", Vector2.ONE * target, duration) \
|
|
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
|
|
|
|
|
## Lights up [param container] with [param colour] for a moment, e.g. a row that just
|
|
## changed. A coloured layer is laid over the container and fades away.
|
|
static func flash(container: Container, colour: Color, duration: float = SLOW) -> void:
|
|
if !enabled() || !container.is_inside_tree():
|
|
return
|
|
var layer: ColorRect = _layer(container, colour)
|
|
var flash_tween: Tween = layer.create_tween()
|
|
flash_tween.tween_property(layer, "color:a", 0.0, duration) \
|
|
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
|
flash_tween.tween_callback(layer.queue_free)
|
|
|
|
|
|
## Keeps [param container] glowing in [param colour], softly pulsing, until the returned
|
|
## layer is freed. With animations off the glow stays still.
|
|
static func pulse(container: Container, colour: Color) -> ColorRect:
|
|
var layer: ColorRect = _layer(container, colour)
|
|
layer.name = "Pulse"
|
|
if enabled():
|
|
var pulse_tween: Tween = layer.create_tween().set_loops()
|
|
pulse_tween.tween_property(layer, "color:a", colour.a * 0.3, 0.6) \
|
|
.set_trans(Tween.TRANS_SINE)
|
|
pulse_tween.tween_property(layer, "color:a", colour.a, 0.6) \
|
|
.set_trans(Tween.TRANS_SINE)
|
|
return layer
|
|
|
|
|
|
# A layer of [param colour] over the whole of [param container] that lets clicks through
|
|
static func _layer(container: Container, colour: Color) -> ColorRect:
|
|
var layer: ColorRect = ColorRect.new()
|
|
layer.color = colour
|
|
layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
container.add_child(layer)
|
|
return layer
|