Files
SansanandClaude Opus 5.5 0862d160e5
Build / test (push) Successful in 1m55s
Build / build (push) Successful in 1m12s
Give the sync progress bar colour and animation
The bar in the Sync popup is now an AnimatedProgressBar: taller, with a
darker track and the theme's accent fill (orange, frost blue in Nord, green
with a gold shine for Christmas). It shows Godot's busy animation until the
server reports progress, glides to each new value, has a soft highlight
sweeping along the filled part inside its rounded corners, and fills and
pops before the result shows. With Reduce animations it only jumps to its
values.

The styles are SyncProgressBar, SyncProgressBarFill and SyncProgressBarText
in every theme. Anim.stop stops a keyed animation without starting an empty
one.

The server now sends sync times as "MM:SS"; the spec is updated from
MusicServer develop (also the linux-package route), and the test data and
screenshots use the new format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:13:43 +02:00

146 lines
5.5 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:
stop(node, key)
if !enabled() || !node.is_inside_tree():
return null
var new_tween: Tween = node.create_tween()
node.set_meta(StringName("_tween_" + key), new_tween)
return new_tween
## Stops the animation called [param key] on [param node], if one is running.
static func stop(node: Node, key: StringName = &"anim") -> void:
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()
## 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