local obs = obslua local ffi = require("ffi") -- Win32 API to retrieve global cursor position ffi.cdef[[ typedef struct { long x; long y; } POINT; bool GetCursorPos(POINT* lpPoint); ]] local function get_mouse_position() local pt = ffi.new("POINT") if ffi.C.GetCursorPos(pt) then return pt.x, pt.y end return 0, 0 end local source_info = {} source_info.id = "cursor_png_follower" source_info.type = obs.OBS_SOURCE_TYPE_INPUT source_info.output_flags = obs.OBS_SOURCE_VIDEO source_info.get_name = function() return "Cursor Image Follower" end -- Create Source Data Instance source_info.create = function(settings, source) local data = {} data.context = source data.image_path = "" data.offset_x = 0 data.offset_y = 0 data.image_scale = 1.0 -- Resolution mapping parameters data.screen_w = 1920 data.screen_h = 1080 data.canvas_w = 1280 data.canvas_h = 720 data.manual_scale_x = 1.0 data.manual_scale_y = 1.0 -- Smooth movement parameters data.enable_smooth = false data.smooth_speed = 0.15 data.curr_x = nil data.curr_y = nil data.image_source = nil source_info.update(data, settings) return data end -- Destroy Source Resources source_info.destroy = function(data) if data.image_source then obs.obs_source_release(data.image_source) data.image_source = nil end data = nil end -- Update Source Settings source_info.update = function(data, settings) local new_path = obs.obs_data_get_string(settings, "image_path") data.offset_x = obs.obs_data_get_int(settings, "offset_x") data.offset_y = obs.obs_data_get_int(settings, "offset_y") data.image_scale = obs.obs_data_get_double(settings, "image_scale") -- Resolution and multiplier controls data.screen_w = obs.obs_data_get_int(settings, "screen_w") data.screen_h = obs.obs_data_get_int(settings, "screen_h") data.canvas_w = obs.obs_data_get_int(settings, "canvas_w") data.canvas_h = obs.obs_data_get_int(settings, "canvas_h") data.manual_scale_x = obs.obs_data_get_double(settings, "manual_scale_x") data.manual_scale_y = obs.obs_data_get_double(settings, "manual_scale_y") -- Easing toggle and speed data.enable_smooth = obs.obs_data_get_bool(settings, "enable_smooth") data.smooth_speed = obs.obs_data_get_double(settings, "smooth_speed") if new_path ~= data.image_path then data.image_path = new_path if data.image_source then obs.obs_source_release(data.image_source) data.image_source = nil end if data.image_path ~= "" then local img_settings = obs.obs_data_create() obs.obs_data_set_string(img_settings, "file", data.image_path) obs.obs_data_set_bool(img_settings, "unload", false) data.image_source = obs.obs_source_create("image_source", "cursor_png_tex", img_settings, nil) obs.obs_data_release(img_settings) end end end -- Render Method with Optional Ease In/Out Interpolation source_info.video_render = function(data, effect) if not data.image_source then return end local mx, my = get_mouse_position() -- Screen-to-canvas coordinate mapping local scale_factor_x = (data.canvas_w / math.max(data.screen_w, 1)) * data.manual_scale_x local scale_factor_y = (data.canvas_h / math.max(data.screen_h, 1)) * data.manual_scale_y local target_x = (mx * scale_factor_x) + data.offset_x local target_y = (my * scale_factor_y) + data.offset_y local draw_x = target_x local draw_y = target_y if data.enable_smooth then -- Initialize tracking positions on first run if not data.curr_x or not data.curr_y then data.curr_x = target_x data.curr_y = target_y else -- Linear interpolation (Lerp) for ease in/out effect data.curr_x = data.curr_x + (target_x - data.curr_x) * data.smooth_speed data.curr_y = data.curr_y + (target_y - data.curr_y) * data.smooth_speed end draw_x = data.curr_x draw_y = data.curr_y else -- Reset positions when smooth is turned off data.curr_x = nil data.curr_y = nil end obs.gs_matrix_push() obs.gs_matrix_translate3f(draw_x, draw_y, 0.0) obs.gs_matrix_scale3f(data.image_scale, data.image_scale, 1.0) obs.obs_source_video_render(data.image_source) obs.gs_matrix_pop() end -- Properties UI source_info.get_properties = function(data) local props = obs.obs_properties_create() obs.obs_properties_add_path( props, "image_path", "Image File (PNG):", obs.OBS_PATH_FILE, "Image Files (*.png *.jpg *.jpeg *.bmp *.gif)", nil ) -- Motion Easing Settings obs.obs_properties_add_bool(props, "enable_smooth", "Enable Ease In/Out Motion") obs.obs_properties_add_float_slider(props, "smooth_speed", "Smoothing Speed (Lower = Smoother):", 0.01, 0.50, 0.01) -- Resolution Mapping Settings obs.obs_properties_add_int(props, "screen_w", "Monitor Width (px):", 640, 7680, 1) obs.obs_properties_add_int(props, "screen_h", "Monitor Height (px):", 480, 4320, 1) obs.obs_properties_add_int(props, "canvas_w", "OBS Canvas Width (px):", 640, 7680, 1) obs.obs_properties_add_int(props, "canvas_h", "OBS Canvas Height (px):", 480, 4320, 1) -- Manual Scale Multipliers obs.obs_properties_add_float_slider(props, "manual_scale_x", "Manual Pos Scale X:", 0.1, 3.0, 0.01) obs.obs_properties_add_float_slider(props, "manual_scale_y", "Manual Pos Scale Y:", 0.1, 3.0, 0.01) -- Offsets & Size obs.obs_properties_add_int(props, "offset_x", "Offset X (px):", -2000, 2000, 1) obs.obs_properties_add_int(props, "offset_y", "Offset Y (px):", -2000, 2000, 1) obs.obs_properties_add_float_slider(props, "image_scale", "PNG Image Size:", 0.05, 5.0, 0.05) return props end source_info.get_defaults = function(settings) obs.obs_data_set_default_bool(settings, "enable_smooth", false) obs.obs_data_set_default_double(settings, "smooth_speed", 0.15) obs.obs_data_set_default_int(settings, "screen_w", 1920) obs.obs_data_set_default_int(settings, "screen_h", 1080) obs.obs_data_set_default_int(settings, "canvas_w", 1280) obs.obs_data_set_default_int(settings, "canvas_h", 720) obs.obs_data_set_default_double(settings, "manual_scale_x", 1.0) obs.obs_data_set_default_double(settings, "manual_scale_y", 1.0) obs.obs_data_set_default_int(settings, "offset_x", 0) obs.obs_data_set_default_int(settings, "offset_y", 0) obs.obs_data_set_default_double(settings, "image_scale", 1.0) end function script_description() return "Registers 'Cursor Image Follower' with resolution scaling and ease in/out motion options." end function script_load(settings) obs.obs_register_source(source_info) end