rgt_hud = { types = {}, Element = { animate = function(e, target) if not e.targets then e.targets = {} end local time = minetest.get_us_time() for k, v in pairs(target) do e.targets[k] = { ref = { time = time, value = e[k] }, target = { time = time +(v.duration or 1) *1000000, value = v.value }, ease_fn = v.ease_fn } end end } } local ns = rgt_hud -- Helper function to compute the eased factor (0 to 1) using cubic Bezier interpolation. -- This solves for the parameter u where the x-component matches the input t, -- then returns the corresponding y-component. local function bezier_ease(t, x1, y1, x2, y2) if t <= 0 then return 0 end if t >= 1 then return 1 end -- Binary search to find u such that bezier_x(u) ≈ t local low = 0 local high = 1 local epsilon = 1e-6 local iterations = 0 while (high - low > epsilon) and (iterations < 100) do -- Safeguard max iterations local mid = (low + high) / 2 local x = 3 * mid * (1 - mid) ^ 2 * x1 + 3 * mid ^ 2 * (1 - mid) * x2 + mid ^ 3 if x < t then low = mid else high = mid end iterations = iterations + 1 end local u = (low + high) / 2 -- Compute y at u local y = 3 * u * (1 - u) ^ 2 * y1 + 3 * u ^ 2 * (1 - u) * y2 + u ^ 3 return y end -- Main interpolation function. -- Interpolates between ref and target using weight t (0 to 1), -- eased by the specified cubic Bezier control points (x1, y1, x2, y2). local function interpolate(ref, target, t, x1, y1, x2, y2) -- if type(ref) == "table" then -- local out = {} -- for k, v in pairs(ref) do -- out[k] = interpolate(ref[k], target[k], (el[key] -ref[k]) /(target[k] -ref[k])) -- end -- return out -- end local eased_t = bezier_ease(t, x1 or 0, y1 or 0, x2 or 1, y2 or 1) return ref + (target - ref) * eased_t end --[[ Register a data layout and render delegate for a given HUD type name. { name = "", -- The name of the type. required_fields = {"pos"}, -- A list of the names of fields that must be provided for this element to render properly. field_types = { -- A mapping of field names to their types. Used to notify the API which fields should be treated as vectors. If a field is just a number, it need not be listed. pos = "vec2" }, defaults = { -- Specifies default values for optional fields. color = "#ffffff" }, add = function(self, m) end, -- Called when the element should be added to `m`'s HUD. update = function(self, m, changes) end, -- Called when the element should be visually updated. `changes` is a table containing only the fields that have been changed in the current step, in order to facilitate interaction with hud_change. remove = function(self, m) end, -- Called when the element should be removed from `m`'s HUD. } Types are responsible for managing the actual HUD elements. --]] function ns.register_hud_type(def) ns.types[def.name] = setmetatable(def, {__index = ns.Element}) end function ns.validate_type(elem, type) if not ns.types[elem.type] then warn("Unknown HUD type `"..type.."` for element `"..elem.name.."`; ignoring.") return false end if ns.types[type].required_fields then for _, field in ipairs(ns.types[type].required_fields) do if elem[field] == nil then return false end end end return true end --[[ { name = "name", -- The name of the element. If an element with this name already exists, it will be updated. type = ""|""|"", } --]] function ns.hud_add(m, def) if not ns.validate_type(def, def.type) then return false end local type = ns.types[def.type] if type.defaults then def = extend(table.copy(type.defaults), def) end local el if m.hud[def.name] then el = m.hud[def.name] -- Simply write all modified fields to the existing element. extend(el, def) else el = setmetatable(def, {__index = type}) m.hud[def.name] = el el:add(m) end return el end function ns.update_poi(m) for _, x in pairs(m.poi) do x:remove(m) end m.poi = {} for _, x in ipairs(minetest.find_nodes_in_area(m.pos:offset(-100, -100, -100), m.pos:offset(100,100,100), "group:poi")) do m.poi[#m.poi +1] = ns.hud_add(m, { name = "poi:"..x:to_string(), type = "poi", world_pos = x, }) end end rgt.register_node("poi", { tiles = {"rgt_oak_planks.png^[colorize:#fff:0.5"}, groups = {poi = 1} }) local default_ease_fn = {0,0,1,1} minetest.register_globalstep(function(dtime) local time = minetest.get_us_time() for _, m in pairs(rgt.players) do for k, el in pairs(m.hud) do if el.remove_after then el.remove_after = el.remove_after -dtime if el.remove_after < 0 then el:remove(m) m.hud[k] = nil end end if el.targets and next(el.targets) then local changes = {} for key, target in pairs(el.targets) do local fac = (time -target.ref.time) /(target.target.time -target.ref.time) local ease_fn = target.ease_fn or default_ease_fn local value if el.field_types[key] == "vec2" then value = { x = interpolate(target.ref.value.x, target.target.value.x, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]), y = interpolate(target.ref.value.y, target.target.value.y, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]) } if fac >= 1 then --value.x == target.target.value.x and value.y == target.target.value.y then el.targets[key] = nil end elseif el.field_types[key] == "color" then value = { r = interpolate(target.ref.value.r, target.target.value.r, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]), g = interpolate(target.ref.value.g, target.target.value.g, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]), b = interpolate(target.ref.value.b, target.target.value.b, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]), a = interpolate(target.ref.value.a, target.target.value.a, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]), } if value.r == target.target.value.r and value.g == target.target.value.g and value.b == target.target.value.b and value.a == target.target.value.a then el.targets[key] = nil end else value = interpolate(target.ref.value, target.target.value, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]) if value == target.target.value then el.targets[key] = nil end end el[key] = value -- We could just set this to true, but since we already -- have a new table, we might as well use it. changes[key] = value end el:update(m, changes) end end for k, el in ipairs(m.poi) do if m.dir:distance(m.pos:direction(el.world_pos)) < 0.05 then el.focused = true el:animate { scale = { value = {x=2,y=2}, duration = 0.2, -- ease_fn = {0.42, 0, 1, 1} } } elseif el.focused then el.focused = false el:animate { scale = { value = {x=1,y=1}, duration = 0.2, -- ease_fn = {0.42, 0, 1, 1} } } end end end end) function ns.color_to_number(color) return tonumber(string.format("0x%.2x%.2x%.2x%.2x", color.r, color.g, color.b, color.a)) end -- Pre-register some basic HUD types ns.register_hud_type { name = "text", required_fields = {"pos", "text"}, field_types = { offset = "vec2", pos = "vec2", color = "color" }, defaults = { dir = 0, align = {x=0, y=0}, offset = {x=0, y=0}, color = {r = 0xff, g = 0xff, b = 0xff, a = 0xff} }, add = function(e, m) e._id = m.object:hud_add { type = "text", position = e.pos, direction = e.dir, alignment = e.align, offset = e.offset, -- What does this even do? scale = {x=100, y=100}, text = e.text, number = ns.color_to_number(e.color) } end, update = function(e, m, changes) for k, v in pairs(changes) do if k == "color" then k = "number" v = ns.color_to_number(v) elseif k == "dir" then k = "direction" elseif k == "align" then k = "alignment" end m.object:hud_change(e._id, k, v) end end, remove = function(e, m) m.object:hud_remove(e._id) e._id = nil end } ns.register_hud_type { name = "poi", required_fields = {"world_pos"}, field_types = { scale = "vec2" }, defaults = { scale = {x=1,y=1} }, add = function(e, m) e._id = m.object:hud_add { type = "image_waypoint", scale = e.scale, world_pos = e.world_pos, text = "rgt_acacia_planks.png" } end, update = function(e, m, changes) for k, v in pairs(changes) do if k == "image" then k = "text" end m.object:hud_change(e._id, k, v) end end, remove = function(e, m) m.object:hud_remove(e._id) end } minetest.register_chatcommand("hudtest", { func = function(name) ns.hud_add(rgt.players[name], { name = "test_toast", type = "text", pos = {x=0.5, y=0.9}, offset = {x=0, y=9}, text = "Hello", remove_after = 5 }):animate { offset = { value = {x = 0, y = -100}, duration = 1, ease_fn = {0.42, 0, 0.58, 1} }, color = { value = minetest.colorspec_to_table("#33aaff00"), duration = 2 } } end }) minetest.register_chatcommand("poi", { func = function(name) ns.update_poi(rgt.players[name]) end })