extends Object class_name UITheme ## The one UI theme: high-voltage anime. Built once, applied to the root Window, ## inherited by every Control in every scene. ## ## The look is a charged cel comic — near-black violet ink, hot papaya, and a ## lightning yellow that only ever appears at the moment something is pressed. ## Four rules hold it together: ## ## INK EVERYTHING every panel, chip and letter carries a heavy dark edge. ## It is what makes flat colour read as drawn rather than ## as flat, and it is why text stays legible over a 3D ## scene without a scrim behind it. ## LEAN chips are cut with two sharp corners and two round ones, ## on a diagonal. A uniformly rounded button reads calm; the ## same shape leaning reads fast, and it costs nothing. ## VOLT MEANS NOW yellow is reserved for the pressed state and the bolt. ## Spend it anywhere else and the moment of input stops ## standing out. ## EVERY STATE READS a fill and the text on it are chosen together, and the ## pair is checked. See `STATE_TABLE` and `contrast`. ## ## Everything here is static. Screens ask for `apply_global` once and then use ## the helpers — `heading`, `card`, `divider`, `chip_button` — instead of ## hand-rolling styleboxes, which is how the pause menu drifted 900 lines away ## from the rest of the game's look. ## ## ── Why the state table exists ──────────────────────────────────────────────── ## ## The palette has two light accents (papaya, volt) and one very dark one (ink), ## and a control changes its FILL on hover and press. Text that reads perfectly ## at rest — paper on near-black — inverts to paper-on-yellow the instant the ## pointer arrives, which is a contrast ratio of 1.1:1. It is invisible. ## ## This is the single most common way a stylised UI becomes unreadable, and it is ## also the complaint HoYoverse's own audience has levelled at Zenless Zone ## Zero's menus. The fix is not vigilance, it is arithmetic: `ink_for` picks the ## legible text colour for any fill, every state declares its pair in ## `STATE_TABLE`, and `debug/ui_contrast_check.gd` fails the build if one of them ## drops below the WCAG floor. A state cannot ship unreadable without the check ## going red. const FONT_PATH := "res://assets/ui/fonts/Bangers-Regular.ttf" # ── The voltage palette ────────────────────────────────────────────────────── const INK := Color(0.045, 0.040, 0.075) const INK_SOFT := Color(0.105, 0.095, 0.165) const PANEL := Color(0.075, 0.068, 0.125, 0.94) const PANEL_DEEP := Color(0.045, 0.040, 0.085, 0.97) const PAPER := Color(0.97, 0.96, 0.99) const PAPER_DIM := Color(0.60, 0.58, 0.68) const PAPAYA := Color(1.00, 0.47, 0.10) const PAPAYA_HOT := Color(1.00, 0.64, 0.20) ## Reserved for the instant of input, and for the bolt. See VOLT MEANS NOW. const VOLT := Color(1.00, 0.93, 0.22) const CYAN := Color(0.22, 0.94, 1.00) const MAGENTA := Color(1.00, 0.18, 0.52) ## Older screens name the accent `TEAL`; it is the electric cyan now. const TEAL := CYAN ## Disabled: the fill goes flat and the label desaturates, but it does NOT go so ## dim that it stops being a word. A disabled control still has to say what it ## would do — "Start Match" greyed out is information, an illegible smudge is ## not. This pair sits above 5:1, where the old 0.42-grey sat at 3.2:1. const DEAD_FILL := Color(0.10, 0.10, 0.14, 0.92) const DEAD_EDGE := Color(0.30, 0.29, 0.36) const DEAD_TEXT := Color(0.55, 0.54, 0.62) ## How far a chip leans. Applied as opposite corners round and the other two ## nearly square, which is as close to a skew as a StyleBoxFlat can get. const LEAN := 18 static var _theme: Theme = null # ── Contrast ───────────────────────────────────────────────────────────────── ## Relative luminance, per WCAG 2.1. Alpha is ignored: a chip's fill is drawn ## over the panel behind it, and assuming the fill is opaque is the pessimistic ## reading, which is the one worth checking. static func luminance(c: Color) -> float: var ch := [c.r, c.g, c.b] for i in 3: var v: float = ch[i] ch[i] = v / 12.92 if v <= 0.04045 else pow((v + 0.055) / 1.055, 2.4) return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2] ## WCAG contrast ratio between two colours, 1.0 (identical) .. 21.0 (black on ## white). 4.5 is the floor for body text, 3.0 for text above ~24 px. static func contrast(a: Color, b: Color) -> float: var la := luminance(a) var lb := luminance(b) return (maxf(la, lb) + 0.05) / (minf(la, lb) + 0.05) ## The legible text colour for a given fill: ink on a light chip, paper on a dark ## one. This is what makes a hover safe — the fill is free to become papaya or ## volt because the label follows it. static func ink_for(fill: Color) -> Color: return INK if contrast(INK, fill) >= contrast(PAPER, fill) else PAPER ## Every (fill, text) pair the theme puts on screen, as ## `["/", fill, text, is_large_text]`. ## ## Written out rather than derived so the check has something to compare the ## built theme AGAINST — a table generated from the theme would agree with it by ## construction and catch nothing. Large text (headings, the wordmark, the HUD's ## big numerals) is allowed the 3:1 floor WCAG gives it. static func state_table() -> Array: var out: Array = [] for cls in BUTTON_CLASSES: out.append_array([ [cls + "/normal", INK_SOFT, PAPER, false], [cls + "/hover", PAPAYA, ink_for(PAPAYA), false], [cls + "/pressed", VOLT, ink_for(VOLT), false], [cls + "/focus", INK_SOFT, PAPER, false], [cls + "/disabled", DEAD_FILL, DEAD_TEXT, false], ]) out.append_array([ ["Label/on_panel", PANEL, PAPER, false], ["Label/dim_on_panel", PANEL, PAPER_DIM, false], ["Label/title", PANEL, PAPAYA, true], ["LineEdit/normal", INK_SOFT, PAPER, false], ["LineEdit/placeholder", INK_SOFT, PAPER_DIM, false], ["ItemList/normal", PANEL_DEEP, PAPER, false], ["ItemList/hovered", INK_SOFT, PAPER, false], ["ItemList/selected", PAPAYA, ink_for(PAPAYA), false], ["ItemList/selected_focus", PAPAYA_HOT, ink_for(PAPAYA_HOT), false], ["PopupMenu/normal", PANEL_DEEP, PAPER, false], ["PopupMenu/hover", PAPAYA, ink_for(PAPAYA), false], ["PopupMenu/disabled", PANEL_DEEP, DEAD_TEXT, false], ["TabContainer/selected", PAPAYA, ink_for(PAPAYA), false], ["TabContainer/unselected", INK_SOFT, PAPER_DIM, false], ["TabContainer/hovered", PAPAYA_HOT, ink_for(PAPAYA_HOT), false], ["Tree/normal", PANEL_DEEP, PAPER, false], ["Tree/selected", PAPAYA, ink_for(PAPAYA), false], ["Card/label", INK, PAPER, true], ["Card/hover_label", INK, VOLT, true], ]) return out ## Which theme types get the chip button treatment. Godot theme types do not ## inherit styling from Button, so each one has to be told the same thing. const BUTTON_CLASSES := ["Button", "OptionButton", "MenuButton", "CheckBox", "CheckButton", "LinkButton"] # ── Styleboxes ─────────────────────────────────────────────────────────────── ## A leaning comic chip: flat fill, heavy ink edge, hard offset shadow. ## ## Kept at its original name and argument order — main_menu and match_hud both ## call it — with the lean added on top. static func box(bg: Color, border: Color = INK, radius: int = 10, border_w: int = 3, shadow: bool = true) -> StyleBoxFlat: var sb := StyleBoxFlat.new() sb.bg_color = bg sb.border_color = border sb.set_border_width_all(border_w) sb.corner_radius_top_left = radius + LEAN sb.corner_radius_bottom_right = radius + LEAN sb.corner_radius_top_right = 3 sb.corner_radius_bottom_left = 3 sb.content_margin_left = 22 sb.content_margin_right = 22 sb.content_margin_top = 9 sb.content_margin_bottom = 9 if shadow: sb.shadow_color = Color(INK.r, INK.g, INK.b, 0.7) sb.shadow_size = 3 sb.shadow_offset = Vector2(5, 5) return sb ## A panel: same ink edge, but square-shouldered so it reads as a surface to put ## things on rather than as a very large button. static func panel(bg: Color = PANEL, border: Color = INK, border_w: int = 4) -> StyleBoxFlat: var sb := StyleBoxFlat.new() sb.bg_color = bg sb.border_color = border sb.set_border_width_all(border_w) sb.set_corner_radius_all(6) sb.content_margin_left = 20 sb.content_margin_right = 20 sb.content_margin_top = 16 sb.content_margin_bottom = 16 sb.shadow_color = Color(INK.r, INK.g, INK.b, 0.55) sb.shadow_size = 4 sb.shadow_offset = Vector2(6, 6) return sb ## A tight row chip — a list entry, a popup line, a tab — with the lean dropped. ## ## The lean is energy on a button the eye lands on. Repeated down twenty rows of ## a list it reads as a stack of broken rectangles, so rows get square corners ## and keep the fill and the edge. static func row(bg: Color, border: Color = INK, border_w: int = 2) -> StyleBoxFlat: var sb := StyleBoxFlat.new() sb.bg_color = bg sb.border_color = border sb.set_border_width_all(border_w) sb.set_corner_radius_all(4) sb.content_margin_left = 12 sb.content_margin_right = 12 sb.content_margin_top = 5 sb.content_margin_bottom = 5 return sb static func build() -> Theme: if _theme: return _theme var t := Theme.new() var font: Font = load(FONT_PATH) if ResourceLoader.exists(FONT_PATH) else null if font: t.default_font = font t.default_font_size = 26 # ── Buttons ────────────────────────────────────────────────────────── # Dark chip with a papaya edge at rest; the chip FILLS papaya on hover and # flashes volt on press. The press state drops its shadow, so the chip # visibly slams down into the page rather than just changing colour. Every # state's text colour comes from `ink_for` its own fill, so the label # inverts WITH the chip instead of vanishing into it. for cls in BUTTON_CLASSES: _button_look(t, cls) # ── Labels: ink outline everywhere, for readability straight over 3D ── t.set_color("font_color", "Label", PAPER) t.set_color("font_outline_color", "Label", INK) t.set_constant("outline_size", "Label", 8) t.set_color("default_color", "RichTextLabel", PAPER) t.set_color("font_outline_color", "RichTextLabel", INK) t.set_constant("outline_size", "RichTextLabel", 6) # ── Inputs ─────────────────────────────────────────────────────────── t.set_stylebox("normal", "LineEdit", box(INK_SOFT, PAPAYA, 4, 3, false)) t.set_stylebox("focus", "LineEdit", box(INK_SOFT, CYAN, 4, 3, false)) t.set_stylebox("read_only", "LineEdit", box(DEAD_FILL, DEAD_EDGE, 4, 3, false)) t.set_color("font_color", "LineEdit", PAPER) t.set_color("font_uneditable_color", "LineEdit", DEAD_TEXT) t.set_color("caret_color", "LineEdit", VOLT) t.set_color("font_placeholder_color", "LineEdit", PAPER_DIM) # Selected text: volt fill wants ink glyphs, same rule as a pressed chip. t.set_color("font_selected_color", "LineEdit", INK) t.set_color("selection_color", "LineEdit", VOLT) t.set_stylebox("normal", "SpinBox", box(INK_SOFT, PAPAYA, 4, 3, false)) t.set_color("font_color", "SpinBox", PAPER) # ── Sliders: the filled part is the charged part ───────────────────── t.set_stylebox("slider", "HSlider", box(INK_SOFT, INK, 2, 2, false)) t.set_stylebox("grabber_area", "HSlider", box(PAPAYA, INK, 2, 2, false)) t.set_stylebox("grabber_area_highlight", "HSlider", box(VOLT, INK, 2, 2, false)) t.set_stylebox("slider", "VSlider", box(INK_SOFT, INK, 2, 2, false)) t.set_stylebox("grabber_area", "VSlider", box(PAPAYA, INK, 2, 2, false)) t.set_stylebox("grabber_area_highlight", "VSlider", box(VOLT, INK, 2, 2, false)) # ── Progress bars ──────────────────────────────────────────────────── t.set_stylebox("background", "ProgressBar", row(INK, INK_SOFT, 2)) t.set_stylebox("fill", "ProgressBar", row(PAPAYA, INK, 0)) t.set_color("font_color", "ProgressBar", PAPER) t.set_color("font_outline_color", "ProgressBar", INK) t.set_constant("outline_size", "ProgressBar", 6) # ── Panels / lists ─────────────────────────────────────────────────── t.set_stylebox("panel", "PanelContainer", panel()) t.set_stylebox("panel", "Panel", panel()) t.set_stylebox("panel", "ItemList", panel(PANEL_DEEP, INK, 3)) t.set_color("font_color", "ItemList", PAPER) t.set_color("font_selected_color", "ItemList", ink_for(PAPAYA)) t.set_color("font_hovered_color", "ItemList", PAPER) t.set_color("font_outline_color", "ItemList", INK) t.set_constant("outline_size", "ItemList", 5) t.set_stylebox("selected", "ItemList", row(PAPAYA)) t.set_stylebox("selected_focus", "ItemList", row(PAPAYA_HOT, CYAN)) # Hover keeps PAPER text, so the fill must stay DARK. A papaya hover under a # papaya selection also made the two states indistinguishable — the row you # were pointing at looked exactly like the row you had chosen. t.set_stylebox("hovered", "ItemList", row(INK_SOFT, PAPAYA)) t.set_stylebox("hovered_selected", "ItemList", row(PAPAYA_HOT, CYAN)) t.set_stylebox("cursor", "ItemList", row(Color(0, 0, 0, 0), CYAN)) t.set_stylebox("cursor_unfocused", "ItemList", row(Color(0, 0, 0, 0), PAPER_DIM)) # ── Trees (settings, loadout lists) ────────────────────────────────── t.set_stylebox("panel", "Tree", panel(PANEL_DEEP, INK, 3)) t.set_color("font_color", "Tree", PAPER) t.set_color("font_selected_color", "Tree", ink_for(PAPAYA)) t.set_color("font_outline_color", "Tree", INK) t.set_constant("outline_size", "Tree", 5) t.set_stylebox("selected", "Tree", row(PAPAYA)) t.set_stylebox("selected_focus", "Tree", row(PAPAYA_HOT, CYAN)) t.set_stylebox("hovered", "Tree", row(INK_SOFT, PAPAYA)) t.set_stylebox("hovered_selected", "Tree", row(PAPAYA_HOT, CYAN)) # ── Scrollbars: visible at rest, charged under the thumb ───────────── for cls in ["HScrollBar", "VScrollBar"]: t.set_stylebox("scroll", cls, row(INK, INK_SOFT, 1)) t.set_stylebox("grabber", cls, row(PAPER_DIM, INK, 1)) t.set_stylebox("grabber_highlight", cls, row(PAPAYA, INK, 1)) t.set_stylebox("grabber_pressed", cls, row(VOLT, INK, 1)) # ── Tabs ───────────────────────────────────────────────────────────── t.set_stylebox("panel", "TabContainer", panel()) t.set_stylebox("tab_selected", "TabContainer", box(PAPAYA, INK, 6, 3, false)) t.set_stylebox("tab_unselected", "TabContainer", box(INK_SOFT, INK, 6, 3, false)) t.set_stylebox("tab_hovered", "TabContainer", box(PAPAYA_HOT, INK, 6, 3, false)) t.set_stylebox("tab_disabled", "TabContainer", box(DEAD_FILL, DEAD_EDGE, 6, 3, false)) t.set_color("font_selected_color", "TabContainer", ink_for(PAPAYA)) t.set_color("font_unselected_color", "TabContainer", PAPER_DIM) t.set_color("font_hovered_color", "TabContainer", ink_for(PAPAYA_HOT)) t.set_color("font_disabled_color", "TabContainer", DEAD_TEXT) t.set_color("font_outline_color", "TabContainer", INK) t.set_constant("outline_size", "TabContainer", 5) # ── Popups (OptionButton dropdowns) ────────────────────────────────── # # The dropdown is the one surface where a missing hover colour is fatal: a # PopupMenu draws its hover fill but keeps `font_color` unless # `font_hover_color` is set, so a papaya row under paper text was the least # readable thing in the game and it appeared on every settings menu. t.set_stylebox("panel", "PopupMenu", panel(PANEL_DEEP, PAPAYA, 3)) t.set_stylebox("hover", "PopupMenu", row(PAPAYA, INK, 0)) t.set_color("font_color", "PopupMenu", PAPER) t.set_color("font_hover_color", "PopupMenu", ink_for(PAPAYA)) t.set_color("font_disabled_color", "PopupMenu", DEAD_TEXT) t.set_color("font_accelerator_color", "PopupMenu", PAPER_DIM) t.set_color("font_separator_color", "PopupMenu", PAPAYA) t.set_color("font_outline_color", "PopupMenu", INK) t.set_constant("outline_size", "PopupMenu", 5) # ── Dialogs ────────────────────────────────────────────────────────── t.set_stylebox("panel", "AcceptDialog", panel(PANEL_DEEP, PAPAYA, 4)) t.set_stylebox("embedded_border", "Window", panel(PANEL_DEEP, PAPAYA, 4)) t.set_color("title_color", "Window", VOLT) # ── Tooltips ───────────────────────────────────────────────────────── t.set_stylebox("panel", "TooltipPanel", panel(PANEL_DEEP, VOLT, 2)) t.set_color("font_color", "TooltipLabel", PAPER) t.set_color("font_outline_color", "TooltipLabel", INK) t.set_constant("outline_size", "TooltipLabel", 5) _theme = t return t static func _button_look(t: Theme, cls: String) -> void: t.set_stylebox("normal", cls, box(INK_SOFT, PAPAYA)) t.set_stylebox("hover", cls, box(PAPAYA, INK)) t.set_stylebox("pressed", cls, box(VOLT, INK, 10, 3, false)) # Hover-while-pressed is its own stylebox on a toggle button. Without it a # CheckButton the pointer is over reverts to the hover fill, so a toggle # reads as OFF for as long as you are touching it. t.set_stylebox("hover_pressed", cls, box(VOLT, CYAN, 10, 3, false)) t.set_stylebox("focus", cls, box(INK_SOFT, CYAN)) t.set_stylebox("disabled", cls, box(DEAD_FILL, DEAD_EDGE)) t.set_color("font_color", cls, PAPER) t.set_color("font_hover_color", cls, ink_for(PAPAYA)) t.set_color("font_pressed_color", cls, ink_for(VOLT)) t.set_color("font_hover_pressed_color", cls, ink_for(VOLT)) t.set_color("font_focus_color", cls, PAPER) t.set_color("font_disabled_color", cls, DEAD_TEXT) # NO OUTLINE on a button label. # # An outline exists to separate a glyph from a backdrop the glyph cannot beat # on its own — over the 3D scene, or across a two-tone progress bar. A button # label has neither problem: it sits on a solid chip, and `ink_for` has # already given it a colour that beats that chip. # # Worse, the theme can only carry ONE outline colour per class, and half the # states here use an ink glyph. Ink glyphs inside a 5 px ink outline are not # outlined, they are five pixels fatter — the selected mode chip rendered as # an unreadable dark blob on volt. The chips keep their heavy ink BORDER, so # the drawn look is unaffected. t.set_color("font_outline_color", cls, INK) t.set_constant("outline_size", cls, 0) # The icon has to invert with the label. A paper glyph on a volt chip is the # same 1.1:1 the text would have been, and a CheckBox is ALL icon. t.set_color("icon_normal_color", cls, PAPER) t.set_color("icon_hover_color", cls, ink_for(PAPAYA)) t.set_color("icon_pressed_color", cls, ink_for(VOLT)) t.set_color("icon_hover_pressed_color", cls, ink_for(VOLT)) t.set_color("icon_focus_color", cls, PAPER) t.set_color("icon_disabled_color", cls, DEAD_TEXT) ## Apply the theme to the whole root window. Idempotent, and cheap enough that ## every screen should just call it in `_ready` rather than assuming some other ## screen already did — which is exactly what left the pause menu unstyled ## whenever it was opened before the HUD had loaded. static func apply_global(tree: SceneTree) -> void: if tree and tree.root and tree.root.theme != build(): tree.root.theme = build() # ── Widgets ────────────────────────────────────────────────────────────────── ## The wordmark treatment: papaya on a heavy ink outline, tilted off true. ## ## The tilt is the cheapest energy in the whole theme. Two and a half degrees is ## enough for the eye to register that the type is not sitting square and not ## enough to read as broken. static func title(text: String, size: int = 64, tilt: float = -2.5) -> Label: var l := Label.new() l.text = text l.add_theme_font_size_override("font_size", size) l.add_theme_color_override("font_color", PAPAYA) l.add_theme_color_override("font_outline_color", INK) l.add_theme_constant_override("outline_size", int(maxf(8.0, size * 0.16))) l.rotation_degrees = tilt l.pivot_offset = Vector2(0, size * 0.5) return l ## A section heading — smaller, paper-coloured, no tilt. static func heading(text: String, size: int = 32) -> Label: var l := Label.new() l.text = text l.add_theme_font_size_override("font_size", size) l.add_theme_color_override("font_color", PAPER) l.add_theme_color_override("font_outline_color", INK) l.add_theme_constant_override("outline_size", 8) return l ## A quieter line of body copy — a hint, a port number, a mode's description. static func caption(text: String, size: int = 20) -> Label: var l := Label.new() l.text = text l.add_theme_font_size_override("font_size", size) l.add_theme_color_override("font_color", PAPER_DIM) l.add_theme_color_override("font_outline_color", INK) l.add_theme_constant_override("outline_size", 5) return l ## A bolt-struck rule, for separating sections. static func divider(strike_at: float = 0.34) -> BoltRule: var b := BoltRule.new() b.line_color = PAPAYA b.bolt_color = VOLT b.ink = INK b.strike_at = strike_at return b ## A panel to put things on, already themed. static func card() -> PanelContainer: var p := PanelContainer.new() p.add_theme_stylebox_override("panel", panel()) return p ## The one loud button on a screen — the thing the player came here to press. ## ## Papaya-filled at rest instead of ink-filled, so it is the first thing the eye ## lands on, and correspondingly ink-lettered. Hover goes hotter and press still ## flashes volt, so the state ladder is unchanged; only the resting colour moves. static func primary_button(text: String, size: int = 44) -> Button: var b := Button.new() b.text = text b.add_theme_font_size_override("font_size", size) b.add_theme_stylebox_override("normal", box(PAPAYA, INK, 12, 4)) b.add_theme_stylebox_override("hover", box(PAPAYA_HOT, VOLT, 12, 4)) b.add_theme_stylebox_override("pressed", box(VOLT, INK, 12, 4, false)) b.add_theme_stylebox_override("focus", box(PAPAYA, CYAN, 12, 4)) b.add_theme_color_override("font_color", ink_for(PAPAYA)) b.add_theme_color_override("font_hover_color", ink_for(PAPAYA_HOT)) b.add_theme_color_override("font_pressed_color", ink_for(VOLT)) b.add_theme_color_override("font_focus_color", ink_for(PAPAYA)) b.add_theme_color_override("font_outline_color", Color(INK.r, INK.g, INK.b, 0.55)) b.add_theme_constant_override("outline_size", 4) return b ## Sounds and the hover kick for every button under `root` (recursive). ## ## The kick is a 4% scale-up on hover and a snap back on exit. It is small on ## purpose: the theme already changes the chip's fill colour, and a big scale on ## top of that reads as the button wobbling rather than as it responding. static func wire_sounds(root: Node) -> void: if root == null or not root.is_inside_tree(): return var am = root.get_tree().root.get_node_or_null("AudioManager") for btn in root.find_children("*", "BaseButton", true, false): if btn.has_meta("ui_sfx_wired"): continue btn.set_meta("ui_sfx_wired", true) if am: btn.mouse_entered.connect(func(): am.play_ui("ui_hover")) btn.pressed.connect(func(): am.play_ui("ui_click")) _wire_kick(btn) static func _wire_kick(btn: Control) -> void: btn.mouse_entered.connect(func(): _kick(btn, 1.04)) btn.mouse_exited.connect(func(): _kick(btn, 1.0)) btn.focus_entered.connect(func(): _kick(btn, 1.04)) btn.focus_exited.connect(func(): _kick(btn, 1.0)) static func _kick(btn: Control, to: float) -> void: if not is_instance_valid(btn) or not btn.is_inside_tree(): return # Scale about the chip's own centre, or it grows off to one side. The pivot # is set every time because a container can resize the button after it was # wired, and a stale pivot is what makes a hover look like a lurch. btn.pivot_offset = btn.size * 0.5 var tw := btn.create_tween() # The menu runs while the tree is paused, so the tween has to as well. tw.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS) tw.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tw.tween_property(btn, "scale", Vector2(to, to), 0.12)