Skip to content

Commit fb5fe3a

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

File tree

1 file changed

+140
-53
lines changed

1 file changed

+140
-53
lines changed

obs-zoom-to-mouse.lua

Lines changed: 140 additions & 53 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,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)
831867
end
832868

833869
function 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()
10841117
end
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+
10861156
function 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
11551224
end
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
14271508
end
14281509

14291510
function 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
14621539
end
14631540

14641541
function 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)
14901567
end
14911568

14921569
function 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
15871664
end
15881665

15891666
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
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
15961683
end
15971684

@@ -1612,4 +1699,4 @@ function populate_zoom_sources(list)
16121699

16131700
obs.source_list_release(sources)
16141701
end
1615-
end
1702+
end

0 commit comments

Comments
 (0)