@@ -51,8 +51,6 @@ local osx_mouse_location = nil
5151local use_auto_follow_mouse = true
5252local use_follow_outside_bounds = false
5353local is_following_mouse = false
54- local force_16_9 = true
55- local lock_in = false
5654local follow_speed = 0.1
5755local follow_border = 0
5856local follow_safezone_sensitivity = 10
@@ -89,7 +87,11 @@ local m1, m2 = version:match("(%d+%.%d+)%.(%d+)")
8987local major = tonumber (m1 ) or 0
9088local minor = tonumber (m2 ) or 0
9189
92- local __ar16_9__ = 16 / 9
90+ local keep_shape = false
91+ local auto_start = false
92+ local auto_start_running = false
93+ local aspect_ratio_w = 1
94+ local aspect_ratio_h = 1
9395
9496-- Define the mouse cursor functions for each platform
9597if ffi .os == " Windows" then
@@ -300,6 +302,33 @@ function clamp(min, max, value)
300302 return math.max (min , math.min (max , value ))
301303end
302304
305+ ---
306+ -- Function to calculate GCD (Greatest Common Divisor)
307+ --- @param a number
308+ --- @param b number
309+ function gcd (a , b )
310+ while b ~= 0 do
311+ local temp = b
312+ b = a % b
313+ a = temp
314+ end
315+ return a
316+ end
317+
318+ ---
319+ -- Function to convert resolution to aspect ratio
320+ --- @param width number the width of the resolution
321+ --- @param height number the height of the resolution
322+ --- @return number aspect_width , number aspect_height the simplified aspect ratio as two numbers
323+ function resolution_to_aspect_ratio (width , height )
324+ -- Calculate GCD of width and height
325+ local divisor = gcd (width , height )
326+ -- Simplify width and height using GCD
327+ local aspect_width = width / divisor
328+ local aspect_height = height / divisor
329+ return aspect_width , aspect_height
330+ end
331+
303332---
304333-- Get the size and position of the monitor so that we know the top-left mouse point
305334--- @param source any The OBS source
@@ -462,6 +491,7 @@ function release_sceneitem()
462491 sceneitem_crop_orig = nil
463492 end
464493
494+ toggle_sceneitem_change_listener (false )
465495 obs .obs_sceneitem_release (sceneitem )
466496 sceneitem = nil
467497 end
@@ -545,13 +575,15 @@ function refresh_sceneitem(find_newest)
545575 -- We start at the current scene and use a BFS to look into any nested scenes
546576 local current = obs .obs_scene_from_source (scene_source )
547577 sceneitem = find_scene_item_by_name (current )
578+ toggle_sceneitem_change_listener (true )
548579
549580 obs .obs_source_release (scene_source )
550581 end
551582
552583 if not sceneitem then
553584 log (" WARNING: Source not part of the current scene hierarchy.\n " ..
554585 " Try selecting a different zoom source or switching scenes." )
586+ toggle_sceneitem_change_listener (false )
555587 obs .obs_sceneitem_release (sceneitem )
556588 obs .obs_source_release (source )
557589
@@ -788,8 +820,12 @@ function get_target_position(zoom)
788820 -- Remember that because we are using a crop/pad filter making the size smaller (dividing by zoom) means that we see less of the image
789821 -- in the same amount of space making it look bigger (aka zoomed in)
790822 local new_size = {
791- -- if aspect ratio should be fixed to 16:9, compute width from height instead of getting directly from display size
792- width = (force_16_9 and (zoom .source_size .height * __ar16_9__ ) or zoom .source_size .width ) / zoom .zoom_to ,
823+ width = (
824+ -- if should keep shape, use aspect ratio to get new width
825+ keep_shape and (zoom .source_size .height * (aspect_ratio_w / aspect_ratio_h ))
826+ -- else use source resolution
827+ or zoom .source_size .width
828+ ) / zoom .zoom_to ,
793829 height = zoom .source_size .height / zoom .zoom_to
794830 }
795831
@@ -831,26 +867,11 @@ function on_toggle_follow(pressed)
831867end
832868
833869function on_toggle_zoom (pressed , force_value )
834- if force_value or pressed then
870+ if pressed or force_value then
835871 -- Check if we are in a safe state to zoom
836- if force_value or zoom_state == ZoomState .ZoomedIn or zoom_state == ZoomState .None then
837- local should_zoom
838- if force_value == nil then
839- should_zoom = zoom_state == ZoomState .ZoomedIn or zoom_state == ZoomState .None
840- else
841- should_zoom = force_value
842- end
843-
844- if should_zoom then
845- log (" Zooming in" )
846- -- To zoom in, we get a new target based on where the mouse was when zoom was clicked
847- zoom_state = ZoomState .ZoomingIn
848- zoom_info .zoom_to = zoom_value
849- zoom_time = 0
850- locked_center = nil
851- locked_last_pos = nil
852- zoom_target = get_target_position (zoom_info )
853- else
872+ if zoom_state == ZoomState .ZoomedIn or zoom_state == ZoomState .None or force_value then
873+ local should_zoom = (force_value ~= nil ) and force_value or (zoom_state ~= ZoomState .ZoomedIn )
874+ if not should_zoom then
854875 log (" Zooming out" )
855876 -- To zoom out, we set the target back to whatever it was originally
856877 zoom_state = ZoomState .ZoomingOut
@@ -862,6 +883,18 @@ function on_toggle_zoom(pressed, force_value)
862883 is_following_mouse = false
863884 log (" Tracking mouse is off (due to zoom out)" )
864885 end
886+ else
887+ log (" Zooming in" )
888+ -- To zoom in, we get a new target based on where the mouse was when zoom was clicked
889+ if keep_shape then
890+ update_aspect_ratio ()
891+ end
892+ zoom_state = ZoomState .ZoomingIn
893+ zoom_info .zoom_to = zoom_value
894+ zoom_time = 0
895+ locked_center = nil
896+ locked_last_pos = nil
897+ zoom_target = get_target_position (zoom_info )
865898 end
866899
867900 -- Since we are zooming we need to start the timer for the animation and tracking
@@ -1083,6 +1116,43 @@ function on_transition_start(t)
10831116 release_sceneitem ()
10841117end
10851118
1119+ function update_aspect_ratio ()
1120+ if keep_shape == nil or not keep_shape or sceneitem == nil then
1121+ return
1122+ end
1123+
1124+ sceneitem_info_current = obs .obs_transform_info ()
1125+ obs .obs_sceneitem_get_info (sceneitem , sceneitem_info_current )
1126+ aspect_ratio_w , aspect_ratio_h = resolution_to_aspect_ratio (
1127+ sceneitem_info_current .bounds .x , sceneitem_info_current .bounds .y
1128+ )
1129+ end
1130+
1131+ function on_transform_update ()
1132+ update_aspect_ratio ()
1133+ if keep_shape then
1134+ if zoom_state == ZoomState .ZoomedIn then
1135+ -- Perform zoom again
1136+ zoom_state = ZoomState .ZoomingIn
1137+ zoom_info .zoom_to = zoom_value
1138+ zoom_time = 0
1139+ zoom_target = get_target_position (zoom_info )
1140+ end
1141+ end
1142+ end
1143+
1144+ function toggle_sceneitem_change_listener (value )
1145+ local scene = obs .obs_sceneitem_get_scene (sceneitem )
1146+ local scene_source = obs .obs_scene_get_source (scene )
1147+ local handler = obs .obs_source_get_signal_handler (scene_source )
1148+ if value then
1149+ update_aspect_ratio ()
1150+ obs .signal_handler_connect (handler , " item_transform" , on_transform_update )
1151+ else
1152+ obs .signal_handler_disconnect (handler , on_transform_update )
1153+ end
1154+ end
1155+
10861156function on_frontend_event (event )
10871157 if event == obs .OBS_FRONTEND_EVENT_SCENE_CHANGED then
10881158 log (" OBS Scene changed" )
@@ -1142,15 +1212,14 @@ function on_settings_modified(props, prop, settings)
11421212 local sources_list = obs .obs_properties_get (props , " source" )
11431213 populate_zoom_sources (sources_list )
11441214 return true
1215+ elseif name == " keep_shape" then
1216+ on_transform_update ()
11451217 elseif name == " debug_logs" then
11461218 if obs .obs_data_get_bool (settings , " debug_logs" ) then
11471219 log_current_settings ()
11481220 end
11491221 end
11501222
1151- if lock_in ~= nil then
1152- on_toggle_zoom (true , lock_in )
1153- end
11541223 return false
11551224end
11561225
@@ -1160,6 +1229,8 @@ function log_current_settings()
11601229 local settings = {
11611230 zoom_value = zoom_value ,
11621231 zoom_speed = zoom_speed ,
1232+ auto_start = auto_start ,
1233+ keep_shape = keep_shape ,
11631234 use_auto_follow_mouse = use_auto_follow_mouse ,
11641235 use_follow_outside_bounds = use_follow_outside_bounds ,
11651236 follow_speed = follow_speed ,
@@ -1179,8 +1250,6 @@ function log_current_settings()
11791250 socket_port = socket_port ,
11801251 socket_poll = socket_poll ,
11811252 debug_logs = debug_logs ,
1182- force_16_9 = force_16_9 ,
1183- lock_in = lock_in ,
11841253 version = VERSION
11851254 }
11861255
@@ -1199,8 +1268,9 @@ function on_print_help()
11991268 " Zoom Source: The display capture in the current scene to use for zooming\n " ..
12001269 " Zoom Factor: How much to zoom in by\n " ..
12011270 " Zoom Speed: The speed of the zoom in/out animation\n " ..
1271+ " Zoomed at OBS startup: Start OBS with source zoomed\n " ..
1272+ " Dynamic Aspect Ratio: Adjusst zoom aspect ratio to canvas source size\n " ..
12021273 " Auto follow mouse: True to track the cursor while you are zoomed in\n " ..
1203- " Force 16:9: True to get zoomed window as 16:9 (fixes problems with wide resolutions)\n " ..
12041274 " Follow outside bounds: True to track the cursor even when it is outside the bounds of the source\n " ..
12051275 " Follow Speed: The speed at which the zoomed area will follow the mouse when tracking\n " ..
12061276 " Follow Border: The %distance from the edge of the source that will re-enable mouse tracking\n " ..
@@ -1256,10 +1326,15 @@ function script_properties()
12561326 -- Add the rest of the settings UI
12571327 local zoom = obs .obs_properties_add_float (props , " zoom_value" , " Zoom Factor" , 1 , 5 , 0.5 )
12581328 local zoom_speed = obs .obs_properties_add_float_slider (props , " zoom_speed" , " Zoom Speed" , 0.01 , 1 , 0.01 )
1259- local lock_in = obs .obs_properties_add_bool (props , " lock_in" , " Lock-In " )
1260- obs .obs_property_set_long_description (lock_in ,
1261- " When enabled, auto zoom feature cannot be disabled manually, and auto zoom restarts with OBS too" )
1262- local force_16_9 = obs .obs_properties_add_bool (props , " force_16_9" , " Force 16:9 aspect ratio " )
1329+
1330+ local auto_start = obs .obs_properties_add_bool (props , " auto_start" , " Zoomed at OBS startup " )
1331+ obs .obs_property_set_long_description (auto_start ,
1332+ " When enabled, auto zoom is activated on OBS start up as soon as possible" )
1333+
1334+ local keep_shape = obs .obs_properties_add_bool (props , " keep_shape" , " Dynamic Aspect Ratio " )
1335+ obs .obs_property_set_long_description (keep_shape ,
1336+ " When enabled, zoom will follow he aspect ratio of source in canvas" )
1337+
12631338 local follow = obs .obs_properties_add_bool (props , " follow" , " Auto follow mouse " )
12641339 obs .obs_property_set_long_description (follow ,
12651340 " When enabled mouse traking will auto-start when zoomed in without waiting for tracking toggle hotkey" )
@@ -1375,7 +1450,6 @@ function script_load(settings)
13751450 -- Load any other settings
13761451 zoom_value = obs .obs_data_get_double (settings , " zoom_value" )
13771452 zoom_speed = obs .obs_data_get_double (settings , " zoom_speed" )
1378- use_auto_follow_mouse = obs .obs_data_get_bool (settings , " follow" )
13791453 use_follow_outside_bounds = obs .obs_data_get_bool (settings , " follow_outside_bounds" )
13801454 follow_speed = obs .obs_data_get_double (settings , " follow_speed" )
13811455 follow_border = obs .obs_data_get_int (settings , " follow_border" )
@@ -1395,8 +1469,8 @@ function script_load(settings)
13951469 socket_port = obs .obs_data_get_int (settings , " socket_port" )
13961470 socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
13971471 debug_logs = obs .obs_data_get_bool (settings , " debug_logs" )
1398- lock_in = obs .obs_data_get_bool (settings , " lock_in " )
1399- force_16_9 = obs .obs_data_get_bool (settings , " force_16_9 " )
1472+ auto_start = obs .obs_data_get_bool (settings , " auto_start " )
1473+ keep_shape = obs .obs_data_get_bool (settings , " keep_shape " )
14001474
14011475 obs .obs_frontend_add_event_callback (on_frontend_event )
14021476
@@ -1424,6 +1498,13 @@ function script_load(settings)
14241498 source_name = " "
14251499 use_socket = false
14261500 is_script_loaded = true
1501+
1502+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1503+ log (" Auto starting" )
1504+ auto_start_running = true
1505+ local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
1506+ obs .timer_add (wait_for_auto_start , timer_interval )
1507+ end
14271508end
14281509
14291510function script_unload ()
@@ -1455,10 +1536,6 @@ function script_unload()
14551536 if socket_server ~= nil then
14561537 stop_server ()
14571538 end
1458-
1459- if lock_in then
1460- on_toggle_zoom (true , false )
1461- end
14621539end
14631540
14641541function script_defaults (settings )
@@ -1485,8 +1562,8 @@ function script_defaults(settings)
14851562 obs .obs_data_set_default_int (settings , " socket_port" , 12345 )
14861563 obs .obs_data_set_default_int (settings , " socket_poll" , 10 )
14871564 obs .obs_data_set_default_bool (settings , " debug_logs" , false )
1488- obs .obs_data_set_default_bool (settings , " force_16_9 " , true )
1489- obs .obs_data_set_default_bool (settings , " lock_in " , false )
1565+ obs .obs_data_set_default_bool (settings , " auto_start " , false )
1566+ obs .obs_data_set_default_bool (settings , " keep_shape " , false )
14901567end
14911568
14921569function script_save (settings )
@@ -1543,8 +1620,8 @@ function script_update(settings)
15431620 socket_port = obs .obs_data_get_int (settings , " socket_port" )
15441621 socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
15451622 debug_logs = obs .obs_data_get_bool (settings , " debug_logs" )
1546- force_16_9 = obs .obs_data_get_bool (settings , " force_16_9 " )
1547- lock_in = obs .obs_data_get_bool (settings , " lock_in " )
1623+ auto_start = obs .obs_data_get_bool (settings , " auto_start " )
1624+ keep_shape = obs .obs_data_get_bool (settings , " keep_shape " )
15481625
15491626 -- Only do the expensive refresh if the user selected a new source
15501627 if source_name ~= old_source_name and is_obs_loaded then
@@ -1578,20 +1655,30 @@ function script_update(settings)
15781655 start_server ()
15791656 end
15801657
1581- if lock_in ~= nil and source == nil then
1658+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1659+ log (" Auto starting" )
1660+ auto_start_running = true
15821661 local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
15831662 obs .timer_add (wait_for_auto_start , timer_interval )
1584- elseif lock_in ~= nil and source ~= nil then
1585- on_toggle_zoom (true , lock_in )
15861663 end
15871664end
15881665
15891666function wait_for_auto_start ()
1590- local found_source = obs .obs_get_source_by_name (source_name )
1591- if found_source ~= nil then
1592- source = found_source
1593- on_toggle_zoom (true , lock_in )
1667+ if source_name == " obs-zoom-to-mouse-none" or not auto_start then
15941668 obs .remove_current_callback ()
1669+ auto_start_running = false
1670+ log (" Auto start cancelled" )
1671+ else
1672+ auto_start_running = true
1673+ local found_source = obs .obs_get_source_by_name (source_name )
1674+ if found_source ~= nil then
1675+ -- zoom_state = ZoomState.ZoomingIn
1676+ source = found_source
1677+ on_toggle_zoom (true , true )
1678+ obs .remove_current_callback ()
1679+ auto_start_running = false
1680+ log (" Auto start done" )
1681+ end
15951682 end
15961683end
15971684
0 commit comments