Skip to content

Commit 09ffe66

Browse files
committed
Fixed locked-in, fixed aspect ratio
1 parent 3b8c18d commit 09ffe66

File tree

1 file changed

+150
-52
lines changed

1 file changed

+150
-52
lines changed

obs-zoom-to-mouse.lua

Lines changed: 150 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ local osx_mouse_location = nil
5151
local use_auto_follow_mouse = true
5252
local use_follow_outside_bounds = false
5353
local is_following_mouse = false
54-
local force_16_9 = true
55-
local lock_in = false
5654
local follow_speed = 0.1
5755
local follow_border = 0
5856
local follow_safezone_sensitivity = 10
@@ -89,7 +87,11 @@ local m1, m2 = version:match("(%d+%.%d+)%.(%d+)")
8987
local major = tonumber(m1) or 0
9088
local 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
9597
if ffi.os == "Windows" then
@@ -300,6 +302,33 @@ function clamp(min, max, value)
300302
return math.max(min, math.min(max, value))
301303
end
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,16 +575,19 @@ 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

590+
558591
sceneitem = nil
559592
source = nil
560593
return
@@ -788,8 +821,12 @@ function get_target_position(zoom)
788821
-- 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
789822
-- in the same amount of space making it look bigger (aka zoomed in)
790823
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,
793830
height = zoom.source_size.height / zoom.zoom_to
794831
}
795832

@@ -831,26 +868,11 @@ function on_toggle_follow(pressed)
831868
end
832869

833870
function on_toggle_zoom(pressed, force_value)
834-
if force_value or pressed then
871+
if pressed or force_value then
835872
-- 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
854876
log("Zooming out")
855877
-- To zoom out, we set the target back to whatever it was originally
856878
zoom_state = ZoomState.ZoomingOut
@@ -862,6 +884,18 @@ function on_toggle_zoom(pressed, force_value)
862884
is_following_mouse = false
863885
log("Tracking mouse is off (due to zoom out)")
864886
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)
865899
end
866900

867901
-- Since we are zooming we need to start the timer for the animation and tracking
@@ -1081,6 +1115,53 @@ function on_transition_start(t)
10811115
-- We need to remove the crop from the sceneitem as the transition starts to avoid
10821116
-- a delay with the rendering where you see the old crop and jump to the new one
10831117
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
10841165
end
10851166

10861167
function on_frontend_event(event)
@@ -1142,15 +1223,14 @@ function on_settings_modified(props, prop, settings)
11421223
local sources_list = obs.obs_properties_get(props, "source")
11431224
populate_zoom_sources(sources_list)
11441225
return true
1226+
elseif name == "keep_shape" then
1227+
on_transform_update()
11451228
elseif name == "debug_logs" then
11461229
if obs.obs_data_get_bool(settings, "debug_logs") then
11471230
log_current_settings()
11481231
end
11491232
end
11501233

1151-
if lock_in ~= nil then
1152-
on_toggle_zoom(true, lock_in)
1153-
end
11541234
return false
11551235
end
11561236

@@ -1160,6 +1240,8 @@ function log_current_settings()
11601240
local settings = {
11611241
zoom_value = zoom_value,
11621242
zoom_speed = zoom_speed,
1243+
auto_start = auto_start,
1244+
keep_shape = keep_shape,
11631245
use_auto_follow_mouse = use_auto_follow_mouse,
11641246
use_follow_outside_bounds = use_follow_outside_bounds,
11651247
follow_speed = follow_speed,
@@ -1179,8 +1261,6 @@ function log_current_settings()
11791261
socket_port = socket_port,
11801262
socket_poll = socket_poll,
11811263
debug_logs = debug_logs,
1182-
force_16_9 = force_16_9,
1183-
lock_in = lock_in,
11841264
version = VERSION
11851265
}
11861266

@@ -1199,8 +1279,9 @@ function on_print_help()
11991279
"Zoom Source: The display capture in the current scene to use for zooming\n" ..
12001280
"Zoom Factor: How much to zoom in by\n" ..
12011281
"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" ..
12021284
"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" ..
12041285
"Follow outside bounds: True to track the cursor even when it is outside the bounds of the source\n" ..
12051286
"Follow Speed: The speed at which the zoomed area will follow the mouse when tracking\n" ..
12061287
"Follow Border: The %distance from the edge of the source that will re-enable mouse tracking\n" ..
@@ -1256,10 +1337,15 @@ function script_properties()
12561337
-- Add the rest of the settings UI
12571338
local zoom = obs.obs_properties_add_float(props, "zoom_value", "Zoom Factor", 1, 5, 0.5)
12581339
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+
12631349
local follow = obs.obs_properties_add_bool(props, "follow", "Auto follow mouse ")
12641350
obs.obs_property_set_long_description(follow,
12651351
"When enabled mouse traking will auto-start when zoomed in without waiting for tracking toggle hotkey")
@@ -1375,7 +1461,6 @@ function script_load(settings)
13751461
-- Load any other settings
13761462
zoom_value = obs.obs_data_get_double(settings, "zoom_value")
13771463
zoom_speed = obs.obs_data_get_double(settings, "zoom_speed")
1378-
use_auto_follow_mouse = obs.obs_data_get_bool(settings, "follow")
13791464
use_follow_outside_bounds = obs.obs_data_get_bool(settings, "follow_outside_bounds")
13801465
follow_speed = obs.obs_data_get_double(settings, "follow_speed")
13811466
follow_border = obs.obs_data_get_int(settings, "follow_border")
@@ -1395,8 +1480,8 @@ function script_load(settings)
13951480
socket_port = obs.obs_data_get_int(settings, "socket_port")
13961481
socket_poll = obs.obs_data_get_int(settings, "socket_poll")
13971482
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")
14001485

14011486
obs.obs_frontend_add_event_callback(on_frontend_event)
14021487

@@ -1424,6 +1509,13 @@ function script_load(settings)
14241509
source_name = ""
14251510
use_socket = false
14261511
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
14271519
end
14281520

14291521
function script_unload()
@@ -1455,10 +1547,6 @@ function script_unload()
14551547
if socket_server ~= nil then
14561548
stop_server()
14571549
end
1458-
1459-
if lock_in then
1460-
on_toggle_zoom(true, false)
1461-
end
14621550
end
14631551

14641552
function script_defaults(settings)
@@ -1485,8 +1573,8 @@ function script_defaults(settings)
14851573
obs.obs_data_set_default_int(settings, "socket_port", 12345)
14861574
obs.obs_data_set_default_int(settings, "socket_poll", 10)
14871575
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)
14901578
end
14911579

14921580
function script_save(settings)
@@ -1543,8 +1631,8 @@ function script_update(settings)
15431631
socket_port = obs.obs_data_get_int(settings, "socket_port")
15441632
socket_poll = obs.obs_data_get_int(settings, "socket_poll")
15451633
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")
15481636

15491637
-- Only do the expensive refresh if the user selected a new source
15501638
if source_name ~= old_source_name and is_obs_loaded then
@@ -1578,20 +1666,30 @@ function script_update(settings)
15781666
start_server()
15791667
end
15801668

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
15821672
local timer_interval = math.floor(obs.obs_get_frame_interval_ns() / 100000)
15831673
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)
15861674
end
15871675
end
15881676

15891677
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
15941679
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
15951693
end
15961694
end
15971695

0 commit comments

Comments
 (0)