-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfocursor.lua
More file actions
2254 lines (1919 loc) · 87.8 KB
/
Copy pathfocursor.lua
File metadata and controls
2254 lines (1919 loc) · 87.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local obs = obslua
local VERSION = "0.1.0"
-- FFI安全加载
local ffi = nil
local ffi_loaded = false
local function load_ffi()
if ffi_loaded then
return ffi ~= nil
end
local success, result = pcall(require, "ffi")
if success then
ffi = result
ffi_loaded = true
return true
else
-- 仅在调试模式才输出日志
ffi_loaded = true
return false
end
end
-- ========================================
-- 缩放相关变量
-- ========================================
local source_name = "" -- 用户设置的源名称/模式
local source_name_current = "" -- 当前场景中实际使用的源名称
local source = nil
local sceneitem = nil
local sceneitem_info_orig = nil
local sceneitem_crop_orig = nil
local sceneitem_info = nil
local sceneitem_crop = nil
local crop_filter = nil
local crop_filter_settings = nil
local crop_filter_info_orig = { x = 0.0, y = 0.0, w = 0.0, h = 0.0 }
local crop_filter_info = { x = 0.0, y = 0.0, w = 0.0, h = 0.0 } -- 实际值(double精度)
local crop_filter_info_display = { left = 0, top = 0, right = 0, bottom = 0 } -- 显示值(整数,用于OBS)
local monitor_info = nil
local zoom_info = {
source_size = { width = 0.0, height = 0.0 },
source_crop = { x = 0.0, y = 0.0, w = 0.0, h = 0.0 },
source_crop_filter = { x = 0.0, y = 0.0, w = 0.0, h = 0.0 },
zoom_to = 2.0
}
local zoom_time = 0
local zoom_target = nil
local virtual_window = nil
local zoom_start_position = nil
local locked_center = nil
local locked_last_pos = nil
local hotkey_zoom_id = nil
local hotkey_follow_id = nil
local is_timer_running = false
local CROP_FILTER_NAME = "smooth-cursor-zoom-crop"
-- ========================================
-- 鼠标指针相关变量
-- ========================================
local cursor_source_name = "" -- 用户设置的源名称/模式
local cursor_source_name_current = "" -- 当前场景中实际使用的源名称
local cursor_source = nil
local is_cursor_enabled = false
local cursor_timer_running = false
local hotkey_cursor_id = nil
local current_pos = { x = 0.0, y = 0.0 }
local target_pos = { x = 0.0, y = 0.0 }
local last_mouse_pos = { x = 0.0, y = 0.0 }
local smooth_factor = 0.15
local smooth_factor_clicking = 0.5
local cursor_image_path = ""
local cursor_image_clicking = ""
local cursor_image_dragging = ""
local cursor_scale = 1.0
local click_scale = 1.3
local click_duration = 150
local hotspot_x = 0
local hotspot_y = 0
local click_anchor_x = 0.5
local click_anchor_y = 0.5
local click_start_time = 0
local is_clicking = false
local is_dragging = false
local drag_start_time = 0
local drag_threshold = 100
local current_click_scale = 1.0
local click_animation_active = false
-- 鼠标跟随缩放
local cursor_follow_zoom = true
local cursor_zoom_offset = { x = 0.0, y = 0.0 }
-- 自动缩放功能
local auto_zoom_enabled = false
local auto_zoom_timeout = 3000 -- 自动恢复时间(毫秒)
local auto_zoom_move_threshold = 300 -- 移动阈值(像素)
local auto_zoom_start_time = 0 -- 自动缩放开始时间
local auto_zoom_start_pos = { x = 0.0, y = 0.0 } -- 自动缩放时的鼠标位置
local auto_zoom_active = false -- 是否正在自动缩放中
local auto_zoom_was_clicked = false -- 上一帧是否点击
local auto_zoom_timer_running = false -- 自动缩放定时器是否运行
local auto_zoom_ready = false -- 放大完成,准备开始计时/检测移动
local auto_zoom_threshold_strategy = "wait_complete" -- 阈值检测策略: immediate 或 wait_complete
local auto_zoom_center_follow_on_click = true -- 点击时是否设置/更新基准点
local auto_zoom_center_follow_on_longpress = false -- 长按时是否更新基准点
-- ========================================
-- 配置变量
-- ========================================
local use_auto_follow_mouse = true
local use_follow_outside_bounds = false
local is_following_mouse = false
local follow_speed = 0.25
local follow_border = 8
local follow_safezone_sensitivity = 4
local use_follow_auto_lock = false
local zoom_value = 2
local zoom_speed_in = 0.25 -- 放大速度
local zoom_speed_out = 0.25 -- 缩小速度
local zoom_ease_in_type = "EaseInOut"
local zoom_ease_out_type = "EaseInOut"
local zoom_custom_ease_in_expr = "t * t"
local zoom_custom_ease_out_expr = "t * (2 - t)"
local allow_all_sources = false
local use_monitor_override = false
local monitor_override_x = 0
local monitor_override_y = 0
local monitor_override_w = 1920
local monitor_override_h = 1080
local monitor_override_sx = 1
local monitor_override_sy = 1
local monitor_override_dw = 1920
local monitor_override_dh = 1080
local debug_logs = false
local is_obs_loaded = false
local ZoomState = {
None = 0,
ZoomingIn = 1,
ZoomingOut = 2,
ZoomedIn = 3,
}
local zoom_state = ZoomState.None
-- Windows API
local win_api_available = false
local win_point = nil
-- ========================================
-- 日志函数
-- ========================================
function log(msg)
if debug_logs then
obs.script_log(obs.OBS_LOG_INFO, msg)
end
end
-- ========================================
-- Windows API 初始化
-- ========================================
local function init_windows_api()
if not load_ffi() then
return false
end
if ffi.os ~= "Windows" then
return false
end
local success, error_msg = pcall(function()
ffi.cdef([[
typedef int BOOL;
typedef unsigned short WORD;
typedef short SHORT;
typedef struct {
long x;
long y;
} POINT, *LPPOINT;
BOOL GetCursorPos(LPPOINT);
WORD GetKeyState(int nVirtKey);
SHORT GetAsyncKeyState(int vKey);
]])
win_point = ffi.new("POINT[1]")
end)
if success then
win_api_available = true
log("Windows API initialized successfully")
else
log("Failed to initialize Windows API: " .. tostring(error_msg))
end
return win_api_available
end
-- ========================================
-- 鼠标位置获取
-- ========================================
function get_mouse_pos()
local mouse = { x = 0, y = 0 }
if not win_api_available or not ffi or not win_point then
return mouse
end
local success, result = pcall(function()
if ffi.C.GetCursorPos(win_point) ~= 0 then
return { x = win_point[0].x, y = win_point[0].y }
end
return mouse
end)
if success then
return result
else
return mouse
end
end
function is_mouse_clicked()
if not win_api_available or not ffi then
return false
end
local success, result = pcall(function()
-- GetAsyncKeyState 检查物理按键状态,比 GetKeyState 更适合长跑脚本
-- 返回值是一个 16 位有符号数,最高位 (bit 15) 为 1 表示按键当前处于按下状态
local left_down = ffi.C.GetAsyncKeyState(0x01) < 0
local right_down = ffi.C.GetAsyncKeyState(0x02) < 0
return left_down or right_down
end)
return success and result or false
end
-- ========================================
-- 数学工具函数 & 缓动函数
-- ========================================
function lerp(v0, v1, t)
return v0 * (1 - t) + v1 * t
end
function clamp(min, max, value)
return math.max(min, math.min(max, value))
end
-- 获取当前时间(毫秒),使用 64 位高性能计数器代替 os.clock,避免长寿命脚本溢出或 CPU 时间漂移
function get_time_ms()
-- obs.os_gettime_ns() 返回纳秒,除以 1000000 得到毫秒
return obs.os_gettime_ns() / 1000000
end
-- 缓动函数集合
local Easing = {
Linear = function(t) return t end,
EaseIn = function(t) return t * t end,
EaseOut = function(t) return t * (2 - t) end,
EaseInOut = function(t)
if t < 0.5 then return 2 * t * t else return -1 + (4 - 2 * t) * t end
end,
EaseInCubic = function(t) return t * t * t end,
EaseOutCubic = function(t) return 1 - math.pow(1 - t, 3) end,
EaseInOutCubic = function(t)
if t < 0.5 then return 4 * t * t * t else return 1 - math.pow(-2 * t + 2, 3) / 2 end
end,
EaseInQuart = function(t) return t * t * t * t end,
EaseOutQuart = function(t) return 1 - math.pow(1 - t, 4) end,
EaseInOutQuart = function(t)
if t < 0.5 then return 8 * t * t * t * t else return 1 - math.pow(-2 * t + 2, 4) / 2 end
end,
EaseOutElastic = function(t)
local c4 = (2 * math.pi) / 3
if t == 0 then return 0 end
if t == 1 then return 1 end
return math.pow(2, -10 * t) * math.sin((t * 10 - 0.75) * c4) + 1
end,
EaseOutBack = function(t)
local c1 = 1.70158
local c3 = c1 + 1
return 1 + c3 * math.pow(t - 1, 3) + c1 * math.pow(t - 1, 2)
end,
EaseOutBounce = function(t)
local n1 = 7.5625
local d1 = 2.75
if t < 1 / d1 then
return n1 * t * t
elseif t < 2 / d1 then
t = t - 1.5 / d1
return n1 * t * t + 0.75
elseif t < 2.5 / d1 then
t = t - 2.25 / d1
return n1 * t * t + 0.9375
else
t = t - 2.625 / d1
return n1 * t * t + 0.984375
end
end
}
-- 编译自定义函数
local custom_ease_in_func = function(t) return t end
local custom_ease_out_func = function(t) return t end
function create_custom_easing(expr_str)
-- 尝试编译用户输入的表达式
-- 包装在一个接受 t 的函数中,并自动 clamp 结果
local chunk_str = [[
return function(t)
local val = (function() return ]] .. expr_str .. [[ end)()
if type(val) ~= "number" then return t end
return math.max(0, math.min(1, val))
end
]]
local chunk, err = load(chunk_str)
if not chunk then
log("自定义缓动函数编译错误: " .. tostring(err))
return function(t) return t end
end
local success, func = pcall(chunk)
if not success or type(func) ~= "function" then
log("自定义缓动函数加载失败")
return function(t) return t end
end
return func
end
function get_easing_function(name, is_in)
if name == "Custom" then
return is_in and custom_ease_in_func or custom_ease_out_func
end
return Easing[name] or Easing.EaseInOut
end
-- 兼容旧代码引用
function ease_in_out(t) return Easing.EaseInOutCubic(t) end
function ease_out_cubic(t) return Easing.EaseOutCubic(t) end
-- ========================================
-- 监视器信息
-- ========================================
function get_monitor_info(src)
local info = nil
if use_monitor_override then
info = {
x = monitor_override_x,
y = monitor_override_y,
width = monitor_override_w,
height = monitor_override_h,
scale_x = monitor_override_sx,
scale_y = monitor_override_sy,
display_width = monitor_override_dw,
display_height = monitor_override_dh
}
elseif src ~= nil then
local width = obs.obs_source_get_width(src)
local height = obs.obs_source_get_height(src)
if width > 0 and height > 0 then
info = {
x = 0,
y = 0,
width = width,
height = height,
scale_x = 1,
scale_y = 1,
display_width = width,
display_height = height
}
end
end
if not info then
log("WARNING: Could not auto calculate zoom source position and size.")
end
return info
end
-- ========================================
-- 缩放功能
-- ========================================
function release_sceneitem()
if is_timer_running then
obs.timer_remove(on_timer)
is_timer_running = false
end
zoom_state = ZoomState.None
virtual_window = nil
zoom_start_position = nil
-- 重置显示值缓存
crop_filter_info_display = { left = 0, top = 0, right = 0, bottom = 0 }
if sceneitem ~= nil then
-- 不再使用裁剪滤镜,无需移除
crop_filter = nil
crop_filter_settings = nil
if sceneitem_info_orig ~= nil then
obs.obs_sceneitem_set_info(sceneitem, sceneitem_info_orig)
sceneitem_info_orig = nil
end
if sceneitem_crop_orig ~= nil then
obs.obs_sceneitem_set_crop(sceneitem, sceneitem_crop_orig)
sceneitem_crop_orig = nil
end
obs.obs_sceneitem_release(sceneitem)
sceneitem = nil
end
if source ~= nil then
obs.obs_source_release(source)
source = nil
end
end
function set_crop_settings(crop)
-- 使用场景项裁剪和缩放来实现放大效果,而不是使用裁剪滤镜
-- 内部保持双精度浮点数用于计算,只在最后向OBS传递时四舍五入
if sceneitem == nil or sceneitem_info_orig == nil then return end
if zoom_info.source_size.width <= 0 or zoom_info.source_size.height <= 0 then return end
local orig_width = zoom_info.source_size.width
local orig_height = zoom_info.source_size.height
-- 计算裁剪边距(场景项裁剪使用 left, top, right, bottom 边距)
-- 直接从浮点数计算,然后向上取整,以保持最终显示值的一致性
local crop_left_float = crop.x
local crop_top_float = crop.y
local crop_right_float = orig_width - crop.x - crop.w
local crop_bottom_float = orig_height - crop.y - crop.h
-- 向上取整到整数(显示值)
local crop_left = math.ceil(crop_left_float)
local crop_top = math.ceil(crop_top_float)
local crop_right = math.ceil(crop_right_float)
local crop_bottom = math.ceil(crop_bottom_float)
-- 只在实际值改变时才更新OBS
if crop_filter_info_display.left ~= crop_left or
crop_filter_info_display.top ~= crop_top or
crop_filter_info_display.right ~= crop_right or
crop_filter_info_display.bottom ~= crop_bottom then
crop_filter_info_display.left = crop_left
crop_filter_info_display.top = crop_top
crop_filter_info_display.right = crop_right
crop_filter_info_display.bottom = crop_bottom
-- 设置场景项裁剪
local new_crop = obs.obs_sceneitem_crop()
new_crop.left = crop_left
new_crop.top = crop_top
new_crop.right = crop_right
new_crop.bottom = crop_bottom
obs.obs_sceneitem_set_crop(sceneitem, new_crop)
-- 计算缩放比例:原始尺寸 / 裁剪后尺寸
-- 重要:必须基于取整后的裁剪值计算,确保裁剪和缩放完全一致
local cropped_width = orig_width - crop_left - crop_right
local cropped_height = orig_height - crop_top - crop_bottom
-- 防止除以零
if cropped_width <= 0 or cropped_height <= 0 then return end
local scale_x = orig_width / cropped_width
local scale_y = orig_height / cropped_height
-- 获取当前变换信息并更新缩放
local new_info = obs.obs_transform_info()
obs.obs_sceneitem_get_info(sceneitem, new_info)
-- 应用新的缩放(原始缩放 * 放大倍数)
new_info.scale.x = sceneitem_info_orig.scale.x * scale_x
new_info.scale.y = sceneitem_info_orig.scale.y * scale_y
obs.obs_sceneitem_set_info(sceneitem, new_info)
end
end
-- ========================================
-- 缩放源查找函数(支持模式匹配)
-- ========================================
-- 在当前场景中查找匹配模式的缩放源
-- 模式匹配规则:
-- 1. 精确匹配:source_name
-- 2. 带场景名后缀:source_name_<场景名>
-- 3. 带任意后缀:source_name_*
function find_zoom_source_in_scene()
local scene_source = obs.obs_frontend_get_current_scene()
if not scene_source then
return nil
end
local scene = obs.obs_scene_from_source(scene_source)
if not scene then
obs.obs_source_release(scene_source)
return nil
end
local scene_name = obs.obs_source_get_name(scene_source)
local found_name = nil
-- 如果 source_name 不为空,使用原有的模式匹配逻辑
if source_name ~= "" then
-- 优先级1:精确匹配 source_name
local item = obs.obs_scene_find_source(scene, source_name)
if item then
found_name = source_name
end
-- 优先级2:尝试 source_name_<场景名>
if not found_name and scene_name then
local scene_specific_name = source_name .. "_" .. scene_name
item = obs.obs_scene_find_source(scene, scene_specific_name)
if item then
found_name = scene_specific_name
end
end
-- 优先级3:遍历场景查找以 source_name 开头的源
if not found_name then
local items = obs.obs_scene_enum_items(scene)
if items then
for _, scene_item in ipairs(items) do
local item_source = obs.obs_sceneitem_get_source(scene_item)
if item_source then
local item_name = obs.obs_source_get_name(item_source)
-- 检查是否以 source_name 开头
if item_name and string.sub(item_name, 1, #source_name) == source_name then
found_name = item_name
break
end
end
end
obs.sceneitem_list_release(items)
end
end
else
-- source_name 为空,自动选择第一个窗口/显示器采集源
local items = obs.obs_scene_enum_items(scene)
if items then
for _, scene_item in ipairs(items) do
local item_source = obs.obs_sceneitem_get_source(scene_item)
if item_source then
local item_name = obs.obs_source_get_name(item_source)
local source_id = obs.obs_source_get_id(item_source)
-- 检查是否是窗口/显示器采集源
if source_id == "monitor_capture" or source_id == "display_capture" or source_id == "window_capture" then
found_name = item_name
break
end
end
end
obs.sceneitem_list_release(items)
end
end
obs.obs_source_release(scene_source)
return found_name
end
function get_target_position(zoom)
local mouse = get_mouse_pos()
if monitor_info then
mouse.x = mouse.x - monitor_info.x
mouse.y = mouse.y - monitor_info.y
end
mouse.x = mouse.x - zoom.source_crop_filter.x
mouse.y = mouse.y - zoom.source_crop_filter.y
if monitor_info and monitor_info.scale_x and monitor_info.scale_y then
mouse.x = mouse.x * monitor_info.scale_x
mouse.y = mouse.y * monitor_info.scale_y
end
local new_size = {
width = zoom.source_size.width / zoom.zoom_to,
height = zoom.source_size.height / zoom.zoom_to
}
local pos = {
x = mouse.x - new_size.width * 0.5,
y = mouse.y - new_size.height * 0.5
}
local crop = {
x = pos.x,
y = pos.y,
w = new_size.width,
h = new_size.height,
}
crop.x = clamp(0.0, (zoom.source_size.width - new_size.width), crop.x)
crop.y = clamp(0.0, (zoom.source_size.height - new_size.height), crop.y)
return { crop = crop, raw_center = mouse, clamped_center = { x = crop.x + crop.w * 0.5, y = crop.y + crop.h * 0.5 } }
end
function refresh_sceneitem(find_newest)
if find_newest then
release_sceneitem()
-- 使用模式匹配查找缩放源(包括空名称时的自动选择)
local found_name = find_zoom_source_in_scene()
if not found_name then
local mode_desc = source_name == "" and "自动选择模式" or ("模式: " .. source_name)
log("未找到匹配的缩放源 (" .. mode_desc .. ")")
return
end
source_name_current = found_name
log("Finding sceneitem for Zoom Source '" .. source_name_current .. "'")
source = obs.obs_get_source_by_name(source_name_current)
if source == nil then
log("Source not found: " .. source_name_current)
return
end
local scene_source = obs.obs_frontend_get_current_scene()
if scene_source then
local scene = obs.obs_scene_from_source(scene_source)
if scene then
local item = obs.obs_scene_find_source(scene, source_name_current)
if item then
sceneitem = item
obs.obs_sceneitem_addref(sceneitem)
end
end
obs.obs_source_release(scene_source)
end
end
if not monitor_info then
monitor_info = get_monitor_info(source)
end
if sceneitem ~= nil then
sceneitem_info_orig = obs.obs_transform_info()
obs.obs_sceneitem_get_info(sceneitem, sceneitem_info_orig)
-- 如果场景项设置了边界框,需要临时禁用,否则裁剪后OBS会自动缩放场景项
if sceneitem_info_orig.bounds_type ~= obs.OBS_BOUNDS_NONE then
local new_info = obs.obs_transform_info()
obs.obs_sceneitem_get_info(sceneitem, new_info)
new_info.bounds_type = obs.OBS_BOUNDS_NONE
obs.obs_sceneitem_set_info(sceneitem, new_info)
log("禁用场景项边界框以防止自动缩放")
end
sceneitem_crop_orig = obs.obs_sceneitem_crop()
obs.obs_sceneitem_get_crop(sceneitem, sceneitem_crop_orig)
local width = obs.obs_source_get_width(source)
local height = obs.obs_source_get_height(source)
zoom_info.source_size.width = width
zoom_info.source_size.height = height
zoom_info.source_crop.x = sceneitem_crop_orig.left
zoom_info.source_crop.y = sceneitem_crop_orig.top
zoom_info.source_crop.w = sceneitem_crop_orig.right
zoom_info.source_crop.h = sceneitem_crop_orig.bottom
zoom_info.source_crop_filter = { x = 0.0, y = 0.0, w = 0.0, h = 0.0 }
-- 不再使用裁剪滤镜,改用场景项裁剪+缩放
crop_filter = nil
crop_filter_settings = nil
crop_filter_info = {
x = 0.0,
y = 0.0,
w = (width + 0.0),
h = (height + 0.0)
}
crop_filter_info_orig = {
x = 0.0,
y = 0.0,
w = (width + 0.0),
h = (height + 0.0)
}
log("Zoom source initialized: " .. width .. "x" .. height)
end
end
-- ========================================
-- on_timer (阻尼追踪模式)
-- ========================================
-- 阈值:当差距小于此值时认为动画完成
local ZOOM_THRESHOLD = 0.5
function on_timer()
if crop_filter_info == nil then return end
-- 确定当前阻尼系数(速度)
local damping = zoom_speed_in
if zoom_state == ZoomState.ZoomingOut then
damping = zoom_speed_out
end
-- 确定目标裁剪区域
local target_crop = nil
if zoom_state == ZoomState.ZoomingOut then
-- 缩小:目标是原始大小
target_crop = {
x = crop_filter_info_orig.x or 0,
y = crop_filter_info_orig.y or 0,
w = zoom_info.source_size.width,
h = zoom_info.source_size.height
}
elseif zoom_state == ZoomState.ZoomingIn then
-- 放大:计算目标位置
local temp_zoom_info = {
source_size = zoom_info.source_size,
source_crop_filter = zoom_info.source_crop_filter,
zoom_to = zoom_info.zoom_to
}
local pos_info = get_target_position(temp_zoom_info)
target_crop = pos_info.crop
elseif zoom_state == ZoomState.ZoomedIn then
-- 已放大:跟随鼠标
if is_following_mouse then
zoom_target = get_target_position(zoom_info)
local skip_frame = false
if not use_follow_outside_bounds then
if zoom_target.raw_center.x < zoom_target.crop.x or
zoom_target.raw_center.x > zoom_target.crop.x + zoom_target.crop.w or
zoom_target.raw_center.y < zoom_target.crop.y or
zoom_target.raw_center.y > zoom_target.crop.y + zoom_target.crop.h then
skip_frame = true
end
end
if not skip_frame then
if locked_center ~= nil then
local diff = {
x = zoom_target.raw_center.x - locked_center.x,
y = zoom_target.raw_center.y - locked_center.y
}
local track = {
x = zoom_target.crop.w * (0.5 - (follow_border * 0.01)),
y = zoom_target.crop.h * (0.5 - (follow_border * 0.01))
}
if math.abs(diff.x) > track.x or math.abs(diff.y) > track.y then
locked_center = nil
locked_last_pos = {
x = zoom_target.raw_center.x,
y = zoom_target.raw_center.y,
diff_x = diff.x,
diff_y = diff.y
}
end
end
if locked_center == nil and (zoom_target.crop.x ~= crop_filter_info.x or zoom_target.crop.y ~= crop_filter_info.y) then
crop_filter_info.x = lerp(crop_filter_info.x, zoom_target.crop.x, follow_speed)
crop_filter_info.y = lerp(crop_filter_info.y, zoom_target.crop.y, follow_speed)
set_crop_settings(crop_filter_info)
if is_following_mouse and locked_center == nil and locked_last_pos ~= nil then
local diff = {
x = math.abs(crop_filter_info.x - zoom_target.crop.x),
y = math.abs(crop_filter_info.y - zoom_target.crop.y),
auto_x = zoom_target.raw_center.x - locked_last_pos.x,
auto_y = zoom_target.raw_center.y - locked_last_pos.y
}
locked_last_pos.x = zoom_target.raw_center.x
locked_last_pos.y = zoom_target.raw_center.y
local lock = false
if math.abs(locked_last_pos.diff_x) > math.abs(locked_last_pos.diff_y) then
if (diff.auto_x < 0 and locked_last_pos.diff_x > 0) or (diff.auto_x > 0 and locked_last_pos.diff_x < 0) then
lock = true
end
else
if (diff.auto_y < 0 and locked_last_pos.diff_y > 0) or (diff.auto_y > 0 and locked_last_pos.diff_y < 0) then
lock = true
end
end
if (lock and use_follow_auto_lock) or (diff.x <= follow_safezone_sensitivity and diff.y <= follow_safezone_sensitivity) then
locked_center = {
x = math.floor(crop_filter_info.x + zoom_target.crop.w * 0.5),
y = math.floor(crop_filter_info.y + zoom_target.crop.h * 0.5)
}
log("Cursor stopped. Tracking locked to " .. locked_center.x .. ", " .. locked_center.y)
end
end
end
end
end
return -- ZoomedIn 状态不需要后续动画完成检测
else
return -- None 状态
end
if target_crop == nil then return end
-- 阻尼追踪:每帧向目标逼近
crop_filter_info.x = lerp(crop_filter_info.x, target_crop.x, damping)
crop_filter_info.y = lerp(crop_filter_info.y, target_crop.y, damping)
crop_filter_info.w = lerp(crop_filter_info.w, target_crop.w, damping)
crop_filter_info.h = lerp(crop_filter_info.h, target_crop.h, damping)
set_crop_settings(crop_filter_info)
-- 检查是否到达目标(阈值判断)
local dx = math.abs(crop_filter_info.x - target_crop.x)
local dy = math.abs(crop_filter_info.y - target_crop.y)
local dw = math.abs(crop_filter_info.w - target_crop.w)
local dh = math.abs(crop_filter_info.h - target_crop.h)
-- 对于 ZoomingIn:只检查尺寸是否到达目标(允许位置继续跟随鼠标)
-- 对于 ZoomingOut:需要位置和尺寸都到达目标
local size_reached = (dw < ZOOM_THRESHOLD and dh < ZOOM_THRESHOLD)
local position_reached = (dx < ZOOM_THRESHOLD and dy < ZOOM_THRESHOLD)
local animation_complete = false
if zoom_state == ZoomState.ZoomingIn then
-- 放大时:尺寸到达目标即可认为放大完成,位置会继续跟随
animation_complete = size_reached
else
-- 缩小时:需要尺寸和位置都到达
animation_complete = size_reached and position_reached
end
if animation_complete then
local should_stop_timer = false
if zoom_state == ZoomState.ZoomingOut then
-- 缩小完成,精确设置到目标值
crop_filter_info.x = target_crop.x
crop_filter_info.y = target_crop.y
crop_filter_info.w = target_crop.w
crop_filter_info.h = target_crop.h
set_crop_settings(crop_filter_info)
log("Zoomed out")
zoom_state = ZoomState.None
virtual_window = nil
zoom_start_position = nil
should_stop_timer = true
elseif zoom_state == ZoomState.ZoomingIn then
-- 放大完成(尺寸到达目标),切换到 ZoomedIn 状态
-- 注意:不精确设置位置,因为位置会继续跟随鼠标
crop_filter_info.w = target_crop.w
crop_filter_info.h = target_crop.h
set_crop_settings(crop_filter_info)
log("Zoomed in")
zoom_state = ZoomState.ZoomedIn
virtual_window = nil
zoom_start_position = nil
should_stop_timer = (not use_auto_follow_mouse) and (not is_following_mouse)
if use_auto_follow_mouse then
is_following_mouse = true
log("Tracking mouse is " .. (is_following_mouse and "on" or "off") .. " (due to auto follow)")
end
if is_following_mouse and follow_border < 50 then
zoom_target = get_target_position(zoom_info)
locked_center = { x = zoom_target.clamped_center.x, y = zoom_target.clamped_center.y }
log("Cursor stopped. Tracking locked to " .. locked_center.x .. ", " .. locked_center.y)
end
end
if should_stop_timer then
is_timer_running = false
obs.timer_remove(on_timer)
end
end
end
-- ========================================
-- 热键回调
-- ========================================
function on_toggle_follow(pressed)
if pressed then
is_following_mouse = not is_following_mouse
log("Tracking mouse is " .. (is_following_mouse and "on" or "off"))
if is_following_mouse and zoom_state == ZoomState.ZoomedIn then
if is_timer_running == false then
is_timer_running = true
local timer_interval = math.floor(obs.obs_get_frame_interval_ns() / 1000000)
obs.timer_add(on_timer, timer_interval)
end
end
end
end
function on_toggle_zoom(pressed)
if pressed then
-- 允许在任何状态(包括动画中)切换
if zoom_state == ZoomState.ZoomedIn or zoom_state == ZoomState.ZoomingIn then
-- 正在放大或已放大 -> 开始缩小
zoom_state = ZoomState.ZoomingOut
locked_center = nil
locked_last_pos = nil
if is_following_mouse then
is_following_mouse = false
end
log("Switching to zoom out")
elseif zoom_state == ZoomState.None or zoom_state == ZoomState.ZoomingOut then
-- 未放大或正在缩小 -> 开始放大
if sceneitem == nil then
refresh_sceneitem(true)
if sceneitem == nil then
local mode_desc = source_name == "" and "自动选择模式" or ("模式: " .. source_name)
log("无法找到缩放源 (" .. mode_desc .. ")")
return
end
end
zoom_state = ZoomState.ZoomingIn
zoom_info.zoom_to = zoom_value
locked_center = nil
locked_last_pos = nil
log("Switching to zoom in, source: " .. (source_name_current or source_name))
end
if is_timer_running == false then
is_timer_running = true
local timer_interval = math.floor(obs.obs_get_frame_interval_ns() / 1000000)
obs.timer_add(on_timer, timer_interval)
end
end
end
-- ========================================
-- 点击动画和拖动检测
-- ========================================
function update_click_animation()
local mouse_clicked = is_mouse_clicked()
local current_time = get_time_ms()
-- 检测拖动状态(按住超过阈值时间)
if mouse_clicked then
if not is_clicking then
is_clicking = true
click_start_time = current_time
drag_start_time = current_time
click_animation_active = true
is_dragging = false
elseif not is_dragging and (current_time - drag_start_time) > drag_threshold then
is_dragging = true
end
else
if is_clicking then
is_clicking = false
is_dragging = false
click_start_time = current_time
end
end
-- click_duration 为 0 时禁用动画
if click_duration <= 0 then
if mouse_clicked then
current_click_scale = click_scale
else
current_click_scale = 1.0
end