diff --git a/README.md b/README.md index 0a68646..6314c63 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ TODO: Issues: 1. **< br />** counts as 1 element in **WebsiteContainer**, therefore despite being (0,0) in size, it counts as double in spacing +2. **Tween** API doesn't modify CSS, it operates independently at Godot level. Notes: - **< input />** is sort-of inline in normal web. We render it as a block element (new-line). diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index 7634fee..0f73ec0 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -797,6 +797,272 @@ var HTML_CONTENT_ADD_REMOVE = """ """.to_utf8_buffer() +var HTML_CONTENT_TWEEN = """ + Tween Animation Demo + + + + + + + + + + +

🎬 Lua Tween Animation Demo

+ +
+

Note: This demo showcases the Lua Tween API for smooth animations. Each box demonstrates different animation properties and easing functions.

+
+ +

Fade Animation

+
+
Fade Me
+
+ + + +
+
+ +

Color Animation

+
+
Change Color
+
+ + +
+
+ +

Scale Animation

+
+
Scale Me
+
+ + + +
+
+ +

Rotation Animation

+
+
Spin Me
+
+ + +
+
+ +

Movement Animation

+
+
Move Me
+
+ + +
+
+ +

Complex Animations

+
+
Complex
+
+ + +
+
+ +
+ +
+ +
+

Available Tween Methods:

+ + +

Supported Properties:

+ + +

Easing Functions:

+ +
+ +""".to_utf8_buffer() + var HTML_CONTENT_DOM_MANIPULATION = """ DOM Utilities Test @@ -2256,3 +2522,7 @@ var HTML_CONTENT = """ """.to_utf8_buffer() + +# Set the active HTML content to use the tween demo +func _ready(): + HTML_CONTENT = HTML_CONTENT_TWEEN diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd index ecbcddf..75e2680 100644 --- a/flumi/Scripts/Utils/Lua/DOM.gd +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -507,6 +507,9 @@ static func add_element_methods(vm: LuauVM, lua_api: LuaAPI) -> void: vm.lua_pushcallable(LuaDOMUtils._element_set_attribute_wrapper, "element.setAttribute") vm.lua_setfield(-2, "setAttribute") + vm.lua_pushcallable(LuaDOMUtils._element_create_tween_wrapper, "element.createTween") + vm.lua_setfield(-2, "createTween") + _add_classlist_support(vm, lua_api) vm.lua_newtable() @@ -1016,6 +1019,14 @@ static func _element_newindex_wrapper(vm: LuauVM) -> int: vm.lua_rawset(1) return 0 +static func _element_create_tween_wrapper(vm: LuauVM) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + vm.lua_pushnil() + return 1 + + return LuaTweenUtils.create_element_tween(vm, lua_api) + static func _unsubscribe_wrapper(vm: LuauVM) -> int: # Get subscription ID from the subscription table vm.luaL_checktype(1, vm.LUA_TTABLE) diff --git a/flumi/Scripts/Utils/Lua/Tween.gd b/flumi/Scripts/Utils/Lua/Tween.gd new file mode 100644 index 0000000..5548612 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Tween.gd @@ -0,0 +1,640 @@ +class_name LuaTweenUtils +extends RefCounted + +class LuaTweenInstance: + var tween: Tween + var target_element: HTMLParser.HTMLElement + var target_dom_node: Node + var lua_api: LuaAPI + var dom_parser: HTMLParser + var tween_chain: Array = [] + var is_parallel: bool = false + var callback_function: Callable + var default_duration: float = 1.0 + var default_easing: String = "easeInOut" + var default_transition: String = "transLinear" + var steps_count: int = 1 + var current_tween_config: Dictionary = {} + + func _init(element: HTMLParser.HTMLElement, dom_node: Node, api: LuaAPI, parser: HTMLParser): + target_element = element + target_dom_node = dom_node + lua_api = api + dom_parser = parser + + # Don't create tween here - will be created on main thread when needed + tween = null + is_parallel = false + +static func create_element_tween(vm: LuauVM, lua_api: LuaAPI) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var element = lua_api.dom_parser.find_by_id(element_id) + if not element: + vm.lua_pushnil() + return 1 + + var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null) + if not dom_node: + vm.lua_pushnil() + return 1 + + var tween_instance = LuaTweenInstance.new(element, dom_node, lua_api, lua_api.dom_parser) + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + + add_tween_methods(vm) + + return 1 + +static func add_tween_methods(vm: LuauVM): + # Animation methods + vm.lua_pushcallable(_tween_to_wrapper, "tween.to") + vm.lua_setfield(-2, "to") + + vm.lua_pushcallable(_tween_from_wrapper, "tween.from") + vm.lua_setfield(-2, "from") + + # Configuration methods + vm.lua_pushcallable(_tween_duration_wrapper, "tween.duration") + vm.lua_setfield(-2, "duration") + + vm.lua_pushcallable(_tween_easing_wrapper, "tween.easing") + vm.lua_setfield(-2, "easing") + + vm.lua_pushcallable(_tween_transition_wrapper, "tween.transition") + vm.lua_setfield(-2, "transition") + + # Control methods + vm.lua_pushcallable(_tween_play_wrapper, "tween.play") + vm.lua_setfield(-2, "play") + + vm.lua_pushcallable(_tween_parallel_wrapper, "tween.parallel") + vm.lua_setfield(-2, "parallel") + + vm.lua_pushcallable(_tween_chain_wrapper, "tween.chain") + vm.lua_setfield(-2, "chain") + + vm.lua_pushcallable(_tween_loops_wrapper, "tween.loops") + vm.lua_setfield(-2, "loops") + + vm.lua_pushcallable(_tween_callback_wrapper, "tween.callback") + vm.lua_setfield(-2, "callback") + + # Playback control + vm.lua_pushcallable(_tween_stop_wrapper, "tween.stop") + vm.lua_setfield(-2, "stop") + + vm.lua_pushcallable(_tween_pause_wrapper, "tween.pause") + vm.lua_setfield(-2, "pause") + + vm.lua_pushcallable(_tween_resume_wrapper, "tween.resume") + vm.lua_setfield(-2, "resume") + + # Kill method + vm.lua_pushcallable(_tween_kill_wrapper, "tween.kill") + vm.lua_setfield(-2, "kill") + +static func _tween_to_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + var property: String = vm.luaL_checkstring(2) + var target_value = vm.lua_tovariant(3) + + var tween_op = { + "type": "to", + "property": property, + "target_value": target_value, + "duration": tween_instance.default_duration, + "easing": tween_instance.default_easing, + "transition": tween_instance.default_transition + } + tween_instance.tween_chain.append(tween_op) + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_from_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + var property: String = vm.luaL_checkstring(2) + var start_value = vm.lua_tovariant(3) + + tween_instance.current_tween_config = { + "type": "from", + "property": property, + "start_value": start_value, + "duration": tween_instance.default_duration, + "easing": tween_instance.default_easing, + "transition": tween_instance.default_transition + } + tween_instance.tween_chain.append(tween_instance.current_tween_config) + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + + # Re-add all tween methods to the new table + add_tween_methods(vm) + + return 1 + +static func _tween_duration_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + var duration: float = vm.luaL_checknumber(2) + + if tween_instance.tween_chain.size() > 0: + tween_instance.tween_chain[-1]["duration"] = duration + else: + tween_instance.default_duration = duration + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_easing_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + var easing: String = vm.luaL_checkstring(2) + + if tween_instance.tween_chain.size() > 0: + tween_instance.tween_chain[-1]["easing"] = easing + else: + tween_instance.default_easing = easing + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_transition_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + var transition: String = vm.luaL_checkstring(2) + + if tween_instance.tween_chain.size() > 0: + tween_instance.tween_chain[-1]["transition"] = transition + else: + tween_instance.default_transition = transition + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_play_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + if not is_instance_valid(tween_instance.target_dom_node): + return 0 + + var operations_to_execute = tween_instance.tween_chain.duplicate() + var is_parallel = tween_instance.is_parallel + var steps_count = tween_instance.steps_count + var callback_func = tween_instance.callback_function + + # Clear chain after copying + tween_instance.tween_chain.clear() + + _execute_tween_on_main_thread.call_deferred(tween_instance.target_dom_node, operations_to_execute, is_parallel, callback_func, steps_count) + + return 0 + +static func _tween_parallel_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + tween_instance.is_parallel = true + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_chain_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + tween_instance.is_parallel = false + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_loops_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + var loops: int = vm.luaL_checkint(2) + + # Store loops count for execution + tween_instance.steps_count = loops + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_callback_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + vm.luaL_checktype(2, vm.LUA_TFUNCTION) + + var callback_ref = vm.lua_ref(2) # Reference function at index 2 + + tween_instance.callback_function = LuaTweenUtils.execute_lua_callback.bind(vm, callback_ref) + + vm.lua_newtable() + vm.lua_pushobject(tween_instance) + vm.lua_setfield(-2, "_tween_instance") + add_tween_methods(vm) + return 1 + +static func _tween_stop_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + if tween_instance.tween and is_instance_valid(tween_instance.tween): + tween_instance.tween.stop() + + return 0 + +static func _tween_pause_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + if tween_instance.tween and is_instance_valid(tween_instance.tween): + tween_instance.tween.pause() + + return 0 + +static func _tween_resume_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + if tween_instance.tween and is_instance_valid(tween_instance.tween): + tween_instance.tween.play() + return 0 + +static func _tween_kill_wrapper(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + vm.lua_getfield(1, "_tween_instance") + var tween_instance = vm.lua_toobject(-1) as LuaTweenInstance + vm.lua_pop(1) + if not tween_instance: + return 0 + + if tween_instance.tween and is_instance_valid(tween_instance.tween): + tween_instance.tween.kill() + return 0 + + +static func parse_easing(easing_name: String) -> Tween.EaseType: + match easing_name.to_lower(): + "in", "easein": + return Tween.EASE_IN + "out", "easeout": + return Tween.EASE_OUT + "inout", "easeinout": + return Tween.EASE_IN_OUT + "outin", "easeoutin": + return Tween.EASE_OUT_IN + _: + return Tween.EASE_IN_OUT + +static func parse_transition(transition_name: String) -> Tween.TransitionType: + match transition_name.to_lower(): + "linear", "translinear": + return Tween.TRANS_LINEAR + "quad", "transquad": + return Tween.TRANS_QUAD + "cubic", "transcubic": + return Tween.TRANS_CUBIC + "quart", "transquart": + return Tween.TRANS_QUART + "quint", "transquint": + return Tween.TRANS_QUINT + "sine", "transsine": + return Tween.TRANS_SINE + "expo", "transexpo": + return Tween.TRANS_EXPO + "circ", "transcirc": + return Tween.TRANS_CIRC + "elastic", "transelastic": + return Tween.TRANS_ELASTIC + "back", "transback": + return Tween.TRANS_BACK + "bounce", "transbounce": + return Tween.TRANS_BOUNCE + _: + return Tween.TRANS_LINEAR + +# Main thread execution of tween operations +static func _execute_tween_on_main_thread(dom_node: Node, operations: Array, is_parallel: bool, callback_func: Callable = Callable(), steps: int = 1): + if not is_instance_valid(dom_node): + return + + var tween = dom_node.create_tween() + if not tween: + return + + tween.set_parallel(is_parallel) + + if steps > 1: + tween.set_loops(steps) + + if callback_func.is_valid(): + tween.finished.connect(callback_func, CONNECT_ONE_SHOT) + + for i in range(operations.size()): + var tween_op = operations[i] + execute_single_tween_operation(dom_node, tween, tween_op) + +static func execute_single_tween_operation(dom_node: Node, tween: Tween, tween_op: Dictionary): + var property = tween_op.property + var duration = tween_op.duration + var easing = parse_easing(tween_op.get("easing", "easeInOut")) + var transition = parse_transition(tween_op.get("transition", "transLinear")) + + match tween_op.type: + "to": + var target_value = tween_op.target_value + animate_property_direct(dom_node, tween, property, target_value, duration, easing, transition) + + "from": + var start_value = tween_op.start_value + var current_value = get_property_value_direct(dom_node, property) + set_property_value_direct(dom_node, property, start_value) + animate_property_direct(dom_node, tween, property, current_value, duration, easing, transition) + +static func animate_property_direct(dom_node: Node, tween: Tween, property: String, target_value, duration: float, easing: Tween.EaseType, transition: Tween.TransitionType = Tween.TRANS_LINEAR): + match property: + "opacity": + var alpha_value = float(target_value) + if dom_node is Control: + var tweener = tween.tween_property(dom_node, "modulate:a", alpha_value, duration) + tweener.set_ease(easing).set_trans(transition) + + "backgroundColor": + var color_value = ColorUtils.parse_color(target_value) + if dom_node is PanelContainer: + var style_box = dom_node.get_theme_stylebox("panel") + if style_box and style_box is StyleBoxFlat: + tween.tween_property(style_box, "bg_color", color_value, duration).set_ease(easing).set_trans(transition) + return + elif dom_node is MarginContainer: + var panel_child = null + for child in dom_node.get_children(): + if child is PanelContainer: + panel_child = child + break + if panel_child: + var style_box = panel_child.get_theme_stylebox("panel") + if style_box and style_box is StyleBoxFlat: + tween.tween_property(style_box, "bg_color", color_value, duration).set_ease(easing).set_trans(transition) + return + tween.tween_property(dom_node, "modulate", color_value, duration).set_ease(easing).set_trans(transition) + # Try Control as fallback + elif dom_node is Control: + tween.tween_property(dom_node, "modulate", color_value, duration).set_ease(easing).set_trans(transition) + + "color", "textColor": + var color_value = ColorUtils.parse_color(target_value) + if dom_node is RichTextLabel: + tween.tween_property(dom_node, "modulate", color_value, duration).set_ease(easing).set_trans(transition) + elif dom_node.has_method("get_node_or_null"): + var rtl = dom_node.get_node_or_null("RichTextLabel") + if rtl: + tween.tween_property(rtl, "modulate", color_value, duration).set_ease(easing).set_trans(transition) + + "width": + var width_value = float(target_value) + if dom_node is Control: + tween.tween_property(dom_node, "custom_minimum_size:x", width_value, duration).set_ease(easing).set_trans(transition) + + "height": + var height_value = float(target_value) + if dom_node is Control: + tween.tween_property(dom_node, "custom_minimum_size:y", height_value, duration).set_ease(easing).set_trans(transition) + + "x", "position.x": + var x_value = float(target_value) + if dom_node is Control: + tween.tween_property(dom_node, "position:x", x_value, duration).set_ease(easing).set_trans(transition) + + "y", "position.y": + var y_value = float(target_value) + if dom_node is Control: + tween.tween_property(dom_node, "position:y", y_value, duration).set_ease(easing).set_trans(transition) + + "scale": + var scale_value = float(target_value) + if dom_node is Control: + var scale_vec = Vector2(scale_value, scale_value) + tween.tween_property(dom_node, "scale", scale_vec, duration).set_ease(easing).set_trans(transition) + + "rotation": + var rotation_value = deg_to_rad(float(target_value)) + if dom_node is Control: + tween.tween_property(dom_node, "rotation", rotation_value, duration).set_ease(easing).set_trans(transition) + +static func set_property_value_direct(dom_node: Node, property: String, value): + match property: + "opacity": + if dom_node is Control: + dom_node.modulate.a = float(value) + "backgroundColor": + var color_value = ColorUtils.parse_color(value) + if dom_node is PanelContainer: + var style_box = dom_node.get_theme_stylebox("panel") + if style_box and style_box is StyleBoxFlat: + style_box.bg_color = color_value + elif dom_node is MarginContainer: + var panel_child = null + for child in dom_node.get_children(): + if child is PanelContainer: + panel_child = child + break + if panel_child: + var style_box = panel_child.get_theme_stylebox("panel") + if style_box and style_box is StyleBoxFlat: + style_box.bg_color = color_value + else: + dom_node.modulate = color_value + elif dom_node is Control: + dom_node.modulate = color_value + "color", "textColor": + var color_value = ColorUtils.parse_color(value) + if dom_node is RichTextLabel: + dom_node.modulate = color_value + elif dom_node.has_method("get_node_or_null"): + var rtl = dom_node.get_node_or_null("RichTextLabel") + if rtl: + rtl.modulate = color_value + "width": + var width_value = float(value) + if dom_node is Control: + dom_node.custom_minimum_size.x = width_value + "height": + var height_value = float(value) + if dom_node is Control: + dom_node.custom_minimum_size.y = height_value + "x", "position.x": + if dom_node is Control: + dom_node.position.x = float(value) + "y", "position.y": + if dom_node is Control: + dom_node.position.y = float(value) + "scale": + var scale_value = float(value) + if dom_node is Control: + var scale_vec = Vector2(scale_value, scale_value) + dom_node.scale = scale_vec + "rotation": + var rotation_value = deg_to_rad(float(value)) + if dom_node is Control: + dom_node.rotation = rotation_value + +static func get_property_value_direct(dom_node: Node, property: String): + match property: + "opacity": + if dom_node is Control: + return dom_node.modulate.a + "backgroundColor": + if dom_node is PanelContainer: + var style_box = dom_node.get_theme_stylebox("panel") + if style_box and style_box is StyleBoxFlat: + return style_box.bg_color + elif dom_node is MarginContainer: + var panel_child = null + for child in dom_node.get_children(): + if child is PanelContainer: + panel_child = child + break + if panel_child: + var style_box = panel_child.get_theme_stylebox("panel") + if style_box and style_box is StyleBoxFlat: + return style_box.bg_color + else: + return dom_node.modulate + elif dom_node is Control: + return dom_node.modulate + "color", "textColor": + if dom_node is RichTextLabel: + return dom_node.modulate + elif dom_node.has_method("get_node_or_null"): + var rtl = dom_node.get_node_or_null("RichTextLabel") + if rtl: + return rtl.modulate + "width": + if dom_node is Control: + return dom_node.custom_minimum_size.x + "height": + if dom_node is Control: + return dom_node.custom_minimum_size.y + "x", "position.x": + if dom_node is Control: + return dom_node.position.x + "y", "position.y": + if dom_node is Control: + return dom_node.position.y + "scale": + if dom_node is Control: + return dom_node.scale.x + "rotation": + if dom_node is Control: + return rad_to_deg(dom_node.rotation) + + return 0 + +static func execute_lua_callback(vm: LuauVM, callback_ref: int): + if not vm or not is_instance_valid(vm): + return + + vm.lua_getref(callback_ref) + + if vm.lua_isfunction(-1): + var result = vm.lua_pcall(0, 0, 0) + if result != vm.LUA_OK: + vm.lua_pop(1) + else: + vm.lua_pop(1) + + vm.lua_unref(callback_ref) diff --git a/flumi/Scripts/Utils/Lua/Tween.gd.uid b/flumi/Scripts/Utils/Lua/Tween.gd.uid new file mode 100644 index 0000000..3ff55a3 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Tween.gd.uid @@ -0,0 +1 @@ +uid://dj2h7hrqd0v0u