@@ -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,13 +575,15 @@ 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
@@ -788,8 +820,12 @@ function get_target_position(zoom)
788
820
-- 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
821
-- in the same amount of space making it look bigger (aka zoomed in)
790
822
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 ,
793
829
height = zoom .source_size .height / zoom .zoom_to
794
830
}
795
831
@@ -831,26 +867,11 @@ function on_toggle_follow(pressed)
831
867
end
832
868
833
869
function on_toggle_zoom (pressed , force_value )
834
- if force_value or pressed then
870
+ if pressed or force_value then
835
871
-- 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
854
875
log (" Zooming out" )
855
876
-- To zoom out, we set the target back to whatever it was originally
856
877
zoom_state = ZoomState .ZoomingOut
@@ -862,6 +883,18 @@ function on_toggle_zoom(pressed, force_value)
862
883
is_following_mouse = false
863
884
log (" Tracking mouse is off (due to zoom out)" )
864
885
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 )
865
898
end
866
899
867
900
-- Since we are zooming we need to start the timer for the animation and tracking
@@ -1083,6 +1116,43 @@ function on_transition_start(t)
1083
1116
release_sceneitem ()
1084
1117
end
1085
1118
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
+
1086
1156
function on_frontend_event (event )
1087
1157
if event == obs .OBS_FRONTEND_EVENT_SCENE_CHANGED then
1088
1158
log (" OBS Scene changed" )
@@ -1142,15 +1212,14 @@ function on_settings_modified(props, prop, settings)
1142
1212
local sources_list = obs .obs_properties_get (props , " source" )
1143
1213
populate_zoom_sources (sources_list )
1144
1214
return true
1215
+ elseif name == " keep_shape" then
1216
+ on_transform_update ()
1145
1217
elseif name == " debug_logs" then
1146
1218
if obs .obs_data_get_bool (settings , " debug_logs" ) then
1147
1219
log_current_settings ()
1148
1220
end
1149
1221
end
1150
1222
1151
- if lock_in ~= nil then
1152
- on_toggle_zoom (true , lock_in )
1153
- end
1154
1223
return false
1155
1224
end
1156
1225
@@ -1160,6 +1229,8 @@ function log_current_settings()
1160
1229
local settings = {
1161
1230
zoom_value = zoom_value ,
1162
1231
zoom_speed = zoom_speed ,
1232
+ auto_start = auto_start ,
1233
+ keep_shape = keep_shape ,
1163
1234
use_auto_follow_mouse = use_auto_follow_mouse ,
1164
1235
use_follow_outside_bounds = use_follow_outside_bounds ,
1165
1236
follow_speed = follow_speed ,
@@ -1179,8 +1250,6 @@ function log_current_settings()
1179
1250
socket_port = socket_port ,
1180
1251
socket_poll = socket_poll ,
1181
1252
debug_logs = debug_logs ,
1182
- force_16_9 = force_16_9 ,
1183
- lock_in = lock_in ,
1184
1253
version = VERSION
1185
1254
}
1186
1255
@@ -1199,8 +1268,9 @@ function on_print_help()
1199
1268
" Zoom Source: The display capture in the current scene to use for zooming\n " ..
1200
1269
" Zoom Factor: How much to zoom in by\n " ..
1201
1270
" 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 " ..
1202
1273
" 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
1274
" Follow outside bounds: True to track the cursor even when it is outside the bounds of the source\n " ..
1205
1275
" Follow Speed: The speed at which the zoomed area will follow the mouse when tracking\n " ..
1206
1276
" Follow Border: The %distance from the edge of the source that will re-enable mouse tracking\n " ..
@@ -1256,10 +1326,15 @@ function script_properties()
1256
1326
-- Add the rest of the settings UI
1257
1327
local zoom = obs .obs_properties_add_float (props , " zoom_value" , " Zoom Factor" , 1 , 5 , 0.5 )
1258
1328
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
+
1263
1338
local follow = obs .obs_properties_add_bool (props , " follow" , " Auto follow mouse " )
1264
1339
obs .obs_property_set_long_description (follow ,
1265
1340
" When enabled mouse traking will auto-start when zoomed in without waiting for tracking toggle hotkey" )
@@ -1375,7 +1450,6 @@ function script_load(settings)
1375
1450
-- Load any other settings
1376
1451
zoom_value = obs .obs_data_get_double (settings , " zoom_value" )
1377
1452
zoom_speed = obs .obs_data_get_double (settings , " zoom_speed" )
1378
- use_auto_follow_mouse = obs .obs_data_get_bool (settings , " follow" )
1379
1453
use_follow_outside_bounds = obs .obs_data_get_bool (settings , " follow_outside_bounds" )
1380
1454
follow_speed = obs .obs_data_get_double (settings , " follow_speed" )
1381
1455
follow_border = obs .obs_data_get_int (settings , " follow_border" )
@@ -1395,8 +1469,8 @@ function script_load(settings)
1395
1469
socket_port = obs .obs_data_get_int (settings , " socket_port" )
1396
1470
socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
1397
1471
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 " )
1400
1474
1401
1475
obs .obs_frontend_add_event_callback (on_frontend_event )
1402
1476
@@ -1424,6 +1498,13 @@ function script_load(settings)
1424
1498
source_name = " "
1425
1499
use_socket = false
1426
1500
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
1427
1508
end
1428
1509
1429
1510
function script_unload ()
@@ -1455,10 +1536,6 @@ function script_unload()
1455
1536
if socket_server ~= nil then
1456
1537
stop_server ()
1457
1538
end
1458
-
1459
- if lock_in then
1460
- on_toggle_zoom (true , false )
1461
- end
1462
1539
end
1463
1540
1464
1541
function script_defaults (settings )
@@ -1485,8 +1562,8 @@ function script_defaults(settings)
1485
1562
obs .obs_data_set_default_int (settings , " socket_port" , 12345 )
1486
1563
obs .obs_data_set_default_int (settings , " socket_poll" , 10 )
1487
1564
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 )
1490
1567
end
1491
1568
1492
1569
function script_save (settings )
@@ -1543,8 +1620,8 @@ function script_update(settings)
1543
1620
socket_port = obs .obs_data_get_int (settings , " socket_port" )
1544
1621
socket_poll = obs .obs_data_get_int (settings , " socket_poll" )
1545
1622
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 " )
1548
1625
1549
1626
-- Only do the expensive refresh if the user selected a new source
1550
1627
if source_name ~= old_source_name and is_obs_loaded then
@@ -1578,20 +1655,30 @@ function script_update(settings)
1578
1655
start_server ()
1579
1656
end
1580
1657
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
1582
1661
local timer_interval = math.floor (obs .obs_get_frame_interval_ns () / 100000 )
1583
1662
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
1663
end
1587
1664
end
1588
1665
1589
1666
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 )
1667
+ if source_name == " obs-zoom-to-mouse-none" or not auto_start then
1594
1668
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
1595
1682
end
1596
1683
end
1597
1684
0 commit comments