@@ -51,8 +51,6 @@ local osx_mouse_location = nil
51
51
local use_auto_follow_mouse = true
52
52
local use_follow_outside_bounds = false
53
53
local is_following_mouse = false
54
- local force_16_9 = true
55
- local lock_in = false
56
54
local follow_speed = 0.1
57
55
local follow_border = 0
58
56
local follow_safezone_sensitivity = 10
@@ -89,7 +87,11 @@ local m1, m2 = version:match("(%d+%.%d+)%.(%d+)")
89
87
local major = tonumber (m1 ) or 0
90
88
local minor = tonumber (m2 ) or 0
91
89
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
93
95
94
96
-- Define the mouse cursor functions for each platform
95
97
if ffi .os == " Windows" then
@@ -300,6 +302,33 @@ function clamp(min, max, value)
300
302
return math.max (min , math.min (max , value ))
301
303
end
302
304
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
+
303
332
---
304
333
-- Get the size and position of the monitor so that we know the top-left mouse point
305
334
--- @param source any The OBS source
@@ -462,6 +491,7 @@ function release_sceneitem()
462
491
sceneitem_crop_orig = nil
463
492
end
464
493
494
+ toggle_sceneitem_change_listener (false )
465
495
obs .obs_sceneitem_release (sceneitem )
466
496
sceneitem = nil
467
497
end
@@ -545,16 +575,19 @@ function refresh_sceneitem(find_newest)
545
575
-- We start at the current scene and use a BFS to look into any nested scenes
546
576
local current = obs .obs_scene_from_source (scene_source )
547
577
sceneitem = find_scene_item_by_name (current )
578
+ toggle_sceneitem_change_listener (true )
548
579
549
580
obs .obs_source_release (scene_source )
550
581
end
551
582
552
583
if not sceneitem then
553
584
log (" WARNING: Source not part of the current scene hierarchy.\n " ..
554
585
" Try selecting a different zoom source or switching scenes." )
586
+ toggle_sceneitem_change_listener (false )
555
587
obs .obs_sceneitem_release (sceneitem )
556
588
obs .obs_source_release (source )
557
589
590
+
558
591
sceneitem = nil
559
592
source = nil
560
593
return
@@ -788,8 +821,12 @@ function get_target_position(zoom)
788
821
-- 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
789
822
-- in the same amount of space making it look bigger (aka zoomed in)
790
823
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 ,
824
+ width = (
825
+ -- if should keep shape, use aspect ratio to get new width
826
+ keep_shape and (zoom .source_size .height * (aspect_ratio_w / aspect_ratio_h ))
827
+ -- else use source resolution
828
+ or zoom .source_size .width
829
+ ) / zoom .zoom_to ,
793
830
height = zoom .source_size .height / zoom .zoom_to
794
831
}
795
832
@@ -831,26 +868,11 @@ function on_toggle_follow(pressed)
831
868
end
832
869
833
870
function on_toggle_zoom (pressed , force_value )
834
- if force_value or pressed then
871
+ if pressed or force_value then
835
872
-- 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
873
+ if zoom_state == ZoomState .ZoomedIn or zoom_state == ZoomState .None or force_value then
874
+ local should_zoom = (force_value ~= nil ) and force_value or (zoom_state ~= ZoomState .ZoomedIn )
875
+ if not should_zoom then
854
876
log (" Zooming out" )
855
877
-- To zoom out, we set the target back to whatever it was originally
856
878
zoom_state = ZoomState .ZoomingOut
@@ -862,6 +884,18 @@ function on_toggle_zoom(pressed, force_value)
862
884
is_following_mouse = false
863
885
log (" Tracking mouse is off (due to zoom out)" )
864
886
end
887
+ else
888
+ log (" Zooming in" )
889
+ -- To zoom in, we get a new target based on where the mouse was when zoom was clicked
890
+ if keep_shape then
891
+ update_aspect_ratio ()
892
+ end
893
+ zoom_state = ZoomState .ZoomingIn
894
+ zoom_info .zoom_to = zoom_value
895
+ zoom_time = 0
896
+ locked_center = nil
897
+ locked_last_pos = nil
898
+ zoom_target = get_target_position (zoom_info )
865
899
end
866
900
867
901
-- Since we are zooming we need to start the timer for the animation and tracking
@@ -1081,6 +1115,53 @@ function on_transition_start(t)
1081
1115
-- We need to remove the crop from the sceneitem as the transition starts to avoid
1082
1116
-- a delay with the rendering where you see the old crop and jump to the new one
1083
1117
release_sceneitem ()
1118
+
1119
+ ---
1120
+ -- Ensure to restart filters on scene change back
1121
+ ---
1122
+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1123
+ log (" Auto starting" )
1124
+ auto_start_running = true
1125
+ local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
1126
+ obs .timer_add (wait_for_auto_start , timer_interval )
1127
+ end
1128
+ end
1129
+
1130
+ function update_aspect_ratio ()
1131
+ if keep_shape == nil or not keep_shape or sceneitem == nil then
1132
+ return
1133
+ end
1134
+
1135
+ sceneitem_info_current = obs .obs_transform_info ()
1136
+ obs .obs_sceneitem_get_info (sceneitem , sceneitem_info_current )
1137
+ aspect_ratio_w , aspect_ratio_h = resolution_to_aspect_ratio (
1138
+ sceneitem_info_current .bounds .x , sceneitem_info_current .bounds .y
1139
+ )
1140
+ end
1141
+
1142
+ function on_transform_update ()
1143
+ update_aspect_ratio ()
1144
+ if keep_shape then
1145
+ if zoom_state == ZoomState .ZoomedIn then
1146
+ -- Perform zoom again
1147
+ zoom_state = ZoomState .ZoomingIn
1148
+ zoom_info .zoom_to = zoom_value
1149
+ zoom_time = 0
1150
+ zoom_target = get_target_position (zoom_info )
1151
+ end
1152
+ end
1153
+ end
1154
+
1155
+ function toggle_sceneitem_change_listener (value )
1156
+ local scene = obs .obs_sceneitem_get_scene (sceneitem )
1157
+ local scene_source = obs .obs_scene_get_source (scene )
1158
+ local handler = obs .obs_source_get_signal_handler (scene_source )
1159
+ if value then
1160
+ update_aspect_ratio ()
1161
+ obs .signal_handler_connect (handler , " item_transform" , on_transform_update )
1162
+ else
1163
+ obs .signal_handler_disconnect (handler , on_transform_update )
1164
+ end
1084
1165
end
1085
1166
1086
1167
function on_frontend_event (event )
@@ -1142,15 +1223,14 @@ function on_settings_modified(props, prop, settings)
1142
1223
local sources_list = obs .obs_properties_get (props , " source" )
1143
1224
populate_zoom_sources (sources_list )
1144
1225
return true
1226
+ elseif name == " keep_shape" then
1227
+ on_transform_update ()
1145
1228
elseif name == " debug_logs" then
1146
1229
if obs .obs_data_get_bool (settings , " debug_logs" ) then
1147
1230
log_current_settings ()
1148
1231
end
1149
1232
end
1150
1233
1151
- if lock_in ~= nil then
1152
- on_toggle_zoom (true , lock_in )
1153
- end
1154
1234
return false
1155
1235
end
1156
1236
@@ -1160,6 +1240,8 @@ function log_current_settings()
1160
1240
local settings = {
1161
1241
zoom_value = zoom_value ,
1162
1242
zoom_speed = zoom_speed ,
1243
+ auto_start = auto_start ,
1244
+ keep_shape = keep_shape ,
1163
1245
use_auto_follow_mouse = use_auto_follow_mouse ,
1164
1246
use_follow_outside_bounds = use_follow_outside_bounds ,
1165
1247
follow_speed = follow_speed ,
@@ -1179,8 +1261,6 @@ function log_current_settings()
1179
1261
socket_port = socket_port ,
1180
1262
socket_poll = socket_poll ,
1181
1263
debug_logs = debug_logs ,
1182
- force_16_9 = force_16_9 ,
1183
- lock_in = lock_in ,
1184
1264
version = VERSION
1185
1265
}
1186
1266
@@ -1199,8 +1279,9 @@ function on_print_help()
1199
1279
" Zoom Source: The display capture in the current scene to use for zooming\n " ..
1200
1280
" Zoom Factor: How much to zoom in by\n " ..
1201
1281
" Zoom Speed: The speed of the zoom in/out animation\n " ..
1282
+ " Zoomed at OBS startup: Start OBS with source zoomed\n " ..
1283
+ " Dynamic Aspect Ratio: Adjusst zoom aspect ratio to canvas source size\n " ..
1202
1284
" 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 " ..
1204
1285
" Follow outside bounds: True to track the cursor even when it is outside the bounds of the source\n " ..
1205
1286
" Follow Speed: The speed at which the zoomed area will follow the mouse when tracking\n " ..
1206
1287
" Follow Border: The %distance from the edge of the source that will re-enable mouse tracking\n " ..
@@ -1256,10 +1337,15 @@ function script_properties()
1256
1337
-- Add the rest of the settings UI
1257
1338
local zoom = obs .obs_properties_add_float (props , " zoom_value" , " Zoom Factor" , 1 , 5 , 0.5 )
1258
1339
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 " )
1340
+
1341
+ local auto_start = obs .obs_properties_add_bool (props , " auto_start" , " Zoomed at OBS startup " )
1342
+ obs .obs_property_set_long_description (auto_start ,
1343
+ " When enabled, auto zoom is activated on OBS start up as soon as possible" )
1344
+
1345
+ local keep_shape = obs .obs_properties_add_bool (props , " keep_shape" , " Dynamic Aspect Ratio " )
1346
+ obs .obs_property_set_long_description (keep_shape ,
1347
+ " When enabled, zoom will follow he aspect ratio of source in canvas" )
1348
+
1263
1349
local follow = obs .obs_properties_add_bool (props , " follow" , " Auto follow mouse " )
1264
1350
obs .obs_property_set_long_description (follow ,
1265
1351
" When enabled mouse traking will auto-start when zoomed in without waiting for tracking toggle hotkey" )
@@ -1375,7 +1461,6 @@ function script_load(settings)
1375
1461
-- Load any other settings
1376
1462
zoom_value = obs .obs_data_get_double (settings , " zoom_value" )
1377
1463
zoom_speed = obs .obs_data_get_double (settings , " zoom_speed" )
1378
- use_auto_follow_mouse = obs .obs_data_get_bool (settings , " follow" )
1379
1464
use_follow_outside_bounds = obs .obs_data_get_bool (settings , " follow_outside_bounds" )
1380
1465
follow_speed = obs .obs_data_get_double (settings , " follow_speed" )
1381
1466
follow_border = obs .obs_data_get_int (settings , " follow_border" )
@@ -1395,8 +1480,8 @@ function script_load(settings)
1395
1480
socket_port = obs .obs_data_get_int (settings , " socket_port" )
1396
1481
socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
1397
1482
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 " )
1483
+ auto_start = obs .obs_data_get_bool (settings , " auto_start " )
1484
+ keep_shape = obs .obs_data_get_bool (settings , " keep_shape " )
1400
1485
1401
1486
obs .obs_frontend_add_event_callback (on_frontend_event )
1402
1487
@@ -1424,6 +1509,13 @@ function script_load(settings)
1424
1509
source_name = " "
1425
1510
use_socket = false
1426
1511
is_script_loaded = true
1512
+
1513
+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1514
+ log (" Auto starting" )
1515
+ auto_start_running = true
1516
+ local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
1517
+ obs .timer_add (wait_for_auto_start , timer_interval )
1518
+ end
1427
1519
end
1428
1520
1429
1521
function script_unload ()
@@ -1455,10 +1547,6 @@ function script_unload()
1455
1547
if socket_server ~= nil then
1456
1548
stop_server ()
1457
1549
end
1458
-
1459
- if lock_in then
1460
- on_toggle_zoom (true , false )
1461
- end
1462
1550
end
1463
1551
1464
1552
function script_defaults (settings )
@@ -1485,8 +1573,8 @@ function script_defaults(settings)
1485
1573
obs .obs_data_set_default_int (settings , " socket_port" , 12345 )
1486
1574
obs .obs_data_set_default_int (settings , " socket_poll" , 10 )
1487
1575
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 )
1576
+ obs .obs_data_set_default_bool (settings , " auto_start " , false )
1577
+ obs .obs_data_set_default_bool (settings , " keep_shape " , false )
1490
1578
end
1491
1579
1492
1580
function script_save (settings )
@@ -1543,8 +1631,8 @@ function script_update(settings)
1543
1631
socket_port = obs .obs_data_get_int (settings , " socket_port" )
1544
1632
socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
1545
1633
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 " )
1634
+ auto_start = obs .obs_data_get_bool (settings , " auto_start " )
1635
+ keep_shape = obs .obs_data_get_bool (settings , " keep_shape " )
1548
1636
1549
1637
-- Only do the expensive refresh if the user selected a new source
1550
1638
if source_name ~= old_source_name and is_obs_loaded then
@@ -1578,20 +1666,30 @@ function script_update(settings)
1578
1666
start_server ()
1579
1667
end
1580
1668
1581
- if lock_in ~= nil and source == nil then
1669
+ if source_name ~= " obs-zoom-to-mouse-none" and auto_start and not auto_start_running then
1670
+ log (" Auto starting" )
1671
+ auto_start_running = true
1582
1672
local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
1583
1673
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 )
1586
1674
end
1587
1675
end
1588
1676
1589
1677
function 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 )
1678
+ if source_name == " obs-zoom-to-mouse-none" or not auto_start then
1594
1679
obs .remove_current_callback ()
1680
+ auto_start_running = false
1681
+ log (" Auto start cancelled" )
1682
+ else
1683
+ auto_start_running = true
1684
+ local found_source = obs .obs_get_source_by_name (source_name )
1685
+ if found_source ~= nil then
1686
+ -- zoom_state = ZoomState.ZoomingIn
1687
+ source = found_source
1688
+ on_toggle_zoom (true , true )
1689
+ obs .remove_current_callback ()
1690
+ auto_start_running = false
1691
+ log (" Auto start done" )
1692
+ end
1595
1693
end
1596
1694
end
1597
1695
0 commit comments