feat: 支持游戏手柄模式 (#562)
101
Cargo.lock
generated
@@ -453,8 +453,9 @@ name = "bongo-cat"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"fs_extra",
|
||||
"gilrs",
|
||||
"input",
|
||||
"nix",
|
||||
"nix 0.30.1",
|
||||
"rdev",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1776,6 +1777,41 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbb2c998745a3c1ac90f64f4f7b3a54219fd3612d7705e7798212935641ed18f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"gilrs-core",
|
||||
"log",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs-core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6d95ae10ce5aa99543a28cf74e41c11f3b9e3c14f0452bbde46024753cd683e"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"inotify 0.11.0",
|
||||
"io-kit-sys",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"rusty-xinput",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
@@ -2321,6 +2357,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
@@ -2349,6 +2396,16 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0"
|
||||
|
||||
[[package]]
|
||||
name = "io-kit-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
@@ -2639,6 +2696,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macos-accessibility-client"
|
||||
version = "0.0.1"
|
||||
@@ -2801,6 +2867,18 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
@@ -3837,7 +3915,7 @@ dependencies = [
|
||||
"dispatch",
|
||||
"enum-map",
|
||||
"epoll",
|
||||
"inotify",
|
||||
"inotify 0.10.2",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4134,6 +4212,17 @@ version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "rusty-xinput"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3335c2b62e1e48dd927f6c8941705386e3697fa944aabcb10431bea7ee47ef3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
@@ -5796,6 +5885,12 @@ version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -6745,7 +6840,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.30.1",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
|
||||
@@ -37,6 +37,7 @@ tauri-plugin-fs = "2"
|
||||
fs_extra = "1"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
gilrs = { version = "0.11", default-features = false, features = ["xinput"] }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel.workspace = true
|
||||
|
||||
69
src-tauri/assets/models/gamepad/cat.model3.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"FileReferences": {
|
||||
"Moc": "demomodel3.moc3",
|
||||
"Textures": [
|
||||
"demomodel3.1024/texture_00.png",
|
||||
"demomodel3.1024/texture_01.png",
|
||||
"demomodel3.1024/texture_02.png"
|
||||
],
|
||||
"DisplayInfo": "demomodel3.cdi3.json",
|
||||
"Expressions": [
|
||||
{
|
||||
"Name": "live2d_expression0.exp3.json",
|
||||
"File": "live2d_expression0.exp3.json"
|
||||
},
|
||||
{
|
||||
"Name": "live2d_expression1.exp3.json",
|
||||
"File": "live2d_expression1.exp3.json"
|
||||
},
|
||||
{
|
||||
"Name": "live2d_expression2.exp3.json",
|
||||
"File": "live2d_expression2.exp3.json"
|
||||
}
|
||||
],
|
||||
"Motions": {
|
||||
"CAT_motion": [
|
||||
{
|
||||
"File": "live2d_motion1.motion3.json",
|
||||
"Sound": "live2d_motion1.flac",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
},
|
||||
{
|
||||
"File": "live2d_motion2.motion3.json",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
}
|
||||
],
|
||||
"CAT_motion_lock": [
|
||||
{
|
||||
"File": "live2d_motion1.motion3.json",
|
||||
"Sound": "live2d_motion1.flac",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
},
|
||||
{
|
||||
"File": "live2d_motion2.motion3.json",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Groups": [
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Name": "LipSync",
|
||||
"Ids": []
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Name": "EyeBlink",
|
||||
"Ids": [
|
||||
"ParamEyeLOpen",
|
||||
"ParamEyeROpen"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/demomodel3.1024/texture_00.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
src-tauri/assets/models/gamepad/demomodel3.1024/texture_01.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
src-tauri/assets/models/gamepad/demomodel3.1024/texture_02.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
289
src-tauri/assets/models/gamepad/demomodel3.cdi3.json
Normal file
@@ -0,0 +1,289 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "ParamAngleX",
|
||||
"GroupId": "",
|
||||
"Name": "角度 X"
|
||||
},
|
||||
{
|
||||
"Id": "ParamAngleY",
|
||||
"GroupId": "",
|
||||
"Name": "角度 Y"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamLeftHandDown",
|
||||
"GroupId": "",
|
||||
"Name": "左手按下"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamRightHandDown",
|
||||
"GroupId": "",
|
||||
"Name": "右手按下"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickLeftDown",
|
||||
"GroupId": "",
|
||||
"Name": "左摇杆点亮"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickRightDown",
|
||||
"GroupId": "",
|
||||
"Name": "右摇杆点亮"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickShowLeftHand",
|
||||
"GroupId": "",
|
||||
"Name": "显示摇杆左手"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickShowRightHand",
|
||||
"GroupId": "",
|
||||
"Name": "显示摇杆右手"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickLX",
|
||||
"GroupId": "",
|
||||
"Name": "左摇杆X"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickLY",
|
||||
"GroupId": "",
|
||||
"Name": "左摇杆Y"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickRX",
|
||||
"GroupId": "",
|
||||
"Name": "右摇杆X"
|
||||
},
|
||||
{
|
||||
"Id": "CatParamStickRY",
|
||||
"GroupId": "",
|
||||
"Name": "右摇杆Y"
|
||||
},
|
||||
{
|
||||
"Id": "ParamAngleZ",
|
||||
"GroupId": "",
|
||||
"Name": "角度 Z"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeLOpen",
|
||||
"GroupId": "",
|
||||
"Name": "左眼 开闭"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeLSmile",
|
||||
"GroupId": "",
|
||||
"Name": "左眼 微笑"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeROpen",
|
||||
"GroupId": "",
|
||||
"Name": "右眼"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeRSmile",
|
||||
"GroupId": "",
|
||||
"Name": "右眼 微笑"
|
||||
},
|
||||
{
|
||||
"Id": "Param3",
|
||||
"GroupId": "",
|
||||
"Name": "挥手"
|
||||
},
|
||||
{
|
||||
"Id": "Param",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "开启闪电"
|
||||
},
|
||||
{
|
||||
"Id": "Param2",
|
||||
"GroupId": "ParamGroup",
|
||||
"Name": "闪电划过"
|
||||
},
|
||||
{
|
||||
"Id": "Param4",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "表情:thuglife"
|
||||
},
|
||||
{
|
||||
"Id": "Param5",
|
||||
"GroupId": "ParamGroup2",
|
||||
"Name": "表情:升天"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeBallX",
|
||||
"GroupId": "",
|
||||
"Name": "眼球 X"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeBallY",
|
||||
"GroupId": "",
|
||||
"Name": "眼球 Y"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLY",
|
||||
"GroupId": "",
|
||||
"Name": "左眉上下"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRY",
|
||||
"GroupId": "",
|
||||
"Name": "右眉 上下"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLX",
|
||||
"GroupId": "",
|
||||
"Name": "左眉 左右"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRX",
|
||||
"GroupId": "",
|
||||
"Name": "右眉 左右"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLAngle",
|
||||
"GroupId": "",
|
||||
"Name": "左眉 角度"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRAngle",
|
||||
"GroupId": "",
|
||||
"Name": "右眉 角度"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowLForm",
|
||||
"GroupId": "",
|
||||
"Name": "左眉 変形"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBrowRForm",
|
||||
"GroupId": "",
|
||||
"Name": "右眉 変形"
|
||||
},
|
||||
{
|
||||
"Id": "ParamMouthForm",
|
||||
"GroupId": "",
|
||||
"Name": "嘴部 变形"
|
||||
},
|
||||
{
|
||||
"Id": "ParamMouthOpenY",
|
||||
"GroupId": "",
|
||||
"Name": "嘴巴 张开和闭合"
|
||||
},
|
||||
{
|
||||
"Id": "ParamCheek",
|
||||
"GroupId": "",
|
||||
"Name": "脸颊"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBodyAngleX",
|
||||
"GroupId": "",
|
||||
"Name": "身体旋转 X"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBodyAngleY",
|
||||
"GroupId": "",
|
||||
"Name": "身体旋转 Y"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBodyAngleZ",
|
||||
"GroupId": "",
|
||||
"Name": "身体旋转 Z"
|
||||
},
|
||||
{
|
||||
"Id": "ParamBreath",
|
||||
"GroupId": "",
|
||||
"Name": "呼吸"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHairFront",
|
||||
"GroupId": "",
|
||||
"Name": "摇动 前发"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHairSide",
|
||||
"GroupId": "",
|
||||
"Name": "摇动 侧发"
|
||||
},
|
||||
{
|
||||
"Id": "ParamHairBack",
|
||||
"GroupId": "",
|
||||
"Name": "摇动 后发"
|
||||
}
|
||||
],
|
||||
"ParameterGroups": [
|
||||
{
|
||||
"Id": "ParamGroup",
|
||||
"GroupId": "",
|
||||
"Name": "闪电"
|
||||
},
|
||||
{
|
||||
"Id": "ParamGroup2",
|
||||
"GroupId": "",
|
||||
"Name": "表情"
|
||||
}
|
||||
],
|
||||
"Parts": [
|
||||
{
|
||||
"Id": "Part12",
|
||||
"Name": "右手"
|
||||
},
|
||||
{
|
||||
"Id": "Part9",
|
||||
"Name": "左手"
|
||||
},
|
||||
{
|
||||
"Id": "Part11",
|
||||
"Name": "demomodel.psd(未找到对应图层)"
|
||||
},
|
||||
{
|
||||
"Id": "Part7",
|
||||
"Name": "demomodel.psd(未找到对应图层)"
|
||||
},
|
||||
{
|
||||
"Id": "Part3",
|
||||
"Name": "demomodel.psd(未找到对应图层)"
|
||||
},
|
||||
{
|
||||
"Id": "Part2",
|
||||
"Name": "demomodel.psd(未找到对应图层)"
|
||||
},
|
||||
{
|
||||
"Id": "Part",
|
||||
"Name": "demomodel.psd(未找到对应图层)"
|
||||
},
|
||||
{
|
||||
"Id": "Part10",
|
||||
"Name": "天使环"
|
||||
},
|
||||
{
|
||||
"Id": "Part5",
|
||||
"Name": "demomodel.psd(未找到对应图层)"
|
||||
},
|
||||
{
|
||||
"Id": "PartSketch0",
|
||||
"Name": "[ 参考图 ]"
|
||||
},
|
||||
{
|
||||
"Id": "rightstick",
|
||||
"Name": "rightstick"
|
||||
},
|
||||
{
|
||||
"Id": "leftstick",
|
||||
"Name": "leftstick"
|
||||
},
|
||||
{
|
||||
"Id": "Part8",
|
||||
"Name": "thug life"
|
||||
},
|
||||
{
|
||||
"Id": "Part6",
|
||||
"Name": "闪电"
|
||||
},
|
||||
{
|
||||
"Id": "Part4",
|
||||
"Name": "闪电"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/demomodel3.moc3
Normal file
15
src-tauri/assets/models/gamepad/exp_1.exp3.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "ParamEyeLOpen",
|
||||
"Value": 0.321,
|
||||
"Blend": "Multiply"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeROpen",
|
||||
"Value": 0.313,
|
||||
"Blend": "Multiply"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
src-tauri/assets/models/gamepad/exp_2.exp3.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "ParamEyeLOpen",
|
||||
"Value": -1,
|
||||
"Blend": "Add"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"Parameters": []
|
||||
}
|
||||
11
src-tauri/assets/models/gamepad/live2d_expression1.exp3.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"FadeInTime": 0.8,
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "Param4",
|
||||
"Value": 1,
|
||||
"Blend": "Add"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
src-tauri/assets/models/gamepad/live2d_expression2.exp3.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"FadeInTime": 0.5,
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "Param5",
|
||||
"Value": 1,
|
||||
"Blend": "Add"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/live2d_motion1.flac
Normal file
76
src-tauri/assets/models/gamepad/live2d_motion1.motion3.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"Meta": {
|
||||
"Duration": 1.633,
|
||||
"Fps": 30.0,
|
||||
"Loop": true,
|
||||
"AreBeziersRestricted": false,
|
||||
"CurveCount": 2,
|
||||
"TotalSegmentCount": 8,
|
||||
"TotalPointCount": 20,
|
||||
"UserDataCount": 0,
|
||||
"TotalUserDataSize": 0
|
||||
},
|
||||
"Curves": [
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "Param",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.033,
|
||||
0,
|
||||
0.067,
|
||||
1,
|
||||
0.1,
|
||||
1,
|
||||
1,
|
||||
0.411,
|
||||
1,
|
||||
0.722,
|
||||
1,
|
||||
1.033,
|
||||
1,
|
||||
1,
|
||||
1.189,
|
||||
1,
|
||||
1.344,
|
||||
0,
|
||||
1.5,
|
||||
0,
|
||||
0,
|
||||
1.633,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "Param2",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.067,
|
||||
0,
|
||||
1,
|
||||
0.1,
|
||||
0,
|
||||
0.133,
|
||||
0.142,
|
||||
0.167,
|
||||
0.2,
|
||||
1,
|
||||
0.489,
|
||||
0.764,
|
||||
0.811,
|
||||
1,
|
||||
1.133,
|
||||
1,
|
||||
0,
|
||||
1.633,
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
77
src-tauri/assets/models/gamepad/live2d_motion2.motion3.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"Version": 3,
|
||||
"Meta": {
|
||||
"Duration": 2.333,
|
||||
"Fps": 30.0,
|
||||
"Loop": true,
|
||||
"AreBeziersRestricted": true,
|
||||
"CurveCount": 2,
|
||||
"TotalSegmentCount": 7,
|
||||
"TotalPointCount": 21,
|
||||
"UserDataCount": 0,
|
||||
"TotalUserDataSize": 0
|
||||
},
|
||||
"Curves": [
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "CatParamLeftHandDown",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2.333,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"Target": "Parameter",
|
||||
"Id": "Param3",
|
||||
"Segments": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0.133,
|
||||
0,
|
||||
0.267,
|
||||
30,
|
||||
0.4,
|
||||
30,
|
||||
1,
|
||||
0.522,
|
||||
30,
|
||||
0.644,
|
||||
0,
|
||||
0.767,
|
||||
0,
|
||||
1,
|
||||
0.9,
|
||||
0,
|
||||
1.033,
|
||||
30,
|
||||
1.167,
|
||||
30,
|
||||
1,
|
||||
1.3,
|
||||
30,
|
||||
1.433,
|
||||
0,
|
||||
1.567,
|
||||
0,
|
||||
1,
|
||||
1.7,
|
||||
0,
|
||||
1.833,
|
||||
30,
|
||||
1.967,
|
||||
30,
|
||||
1,
|
||||
2.089,
|
||||
30,
|
||||
2.211,
|
||||
0,
|
||||
2.333,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/resources/background.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src-tauri/assets/models/gamepad/resources/cover.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src-tauri/assets/models/gamepad/resources/left-keys/DPadDown.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/left-keys/DPadLeft.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/left-keys/DPadUp.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/East.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/North.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/South.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/West.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
24
src-tauri/assets/models/keyboard/cat.model3.json
Executable file → Normal file
@@ -10,30 +10,40 @@
|
||||
"DisplayInfo": "demomodel2.cdi3.json",
|
||||
"Expressions": [
|
||||
{
|
||||
"Name": "默认喵",
|
||||
"Name": "live2d_expression0.exp3.json",
|
||||
"File": "live2d_expression0.exp3.json"
|
||||
},
|
||||
{
|
||||
"Name": "社会喵",
|
||||
"File": "live2d_expression1.exp3.json",
|
||||
"Description": "喵喵我叼根小烟,耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
|
||||
"Name": "live2d_expression1.exp3.json",
|
||||
"File": "live2d_expression1.exp3.json"
|
||||
},
|
||||
{
|
||||
"Name": "天使喵",
|
||||
"Name": "live2d_expression2.exp3.json",
|
||||
"File": "live2d_expression2.exp3.json"
|
||||
}
|
||||
],
|
||||
"Motions": {
|
||||
"CAT_motion": [
|
||||
{
|
||||
"Name": "雷霆喵",
|
||||
"File": "live2d_motion1.motion3.json",
|
||||
"Sound": "live2d_motion1.flac",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
},
|
||||
{
|
||||
"Name": "摇摆喵",
|
||||
"File": "live2d_motion2.motion3.json",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
}
|
||||
],
|
||||
"CAT_motion_lock": [
|
||||
{
|
||||
"File": "live2d_motion1.motion3.json",
|
||||
"Sound": "live2d_motion1.flac",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
},
|
||||
{
|
||||
"File": "live2d_motion2.motion3.json",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
|
||||
25
src-tauri/assets/models/standard/cat.model3.json
Executable file → Normal file
@@ -10,31 +10,40 @@
|
||||
"DisplayInfo": "demomodel.cdi3.json",
|
||||
"Expressions": [
|
||||
{
|
||||
"Name": "默认喵",
|
||||
"Name": "live2d_expression0.exp3.json",
|
||||
"File": "live2d_expression0.exp3.json"
|
||||
},
|
||||
{
|
||||
"Name": "社会喵",
|
||||
"File": "live2d_expression1.exp3.json",
|
||||
"Description": "喵喵我叼根小烟,耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
|
||||
|
||||
"Name": "live2d_expression1.exp3.json",
|
||||
"File": "live2d_expression1.exp3.json"
|
||||
},
|
||||
{
|
||||
"Name": "天使喵",
|
||||
"Name": "live2d_expression2.exp3.json",
|
||||
"File": "live2d_expression2.exp3.json"
|
||||
}
|
||||
],
|
||||
"Motions": {
|
||||
"CAT_motion": [
|
||||
{
|
||||
"Name": "雷霆喵",
|
||||
"File": "live2d_motion1.motion3.json",
|
||||
"Sound": "live2d_motion1.flac",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
},
|
||||
{
|
||||
"Name": "摇摆喵",
|
||||
"File": "live2d_motion2.motion3.json",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
}
|
||||
],
|
||||
"CAT_motion_lock": [
|
||||
{
|
||||
"File": "live2d_motion1.motion3.json",
|
||||
"Sound": "live2d_motion1.flac",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
},
|
||||
{
|
||||
"File": "live2d_motion2.motion3.json",
|
||||
"FadeInTime": 0,
|
||||
"FadeOutTime": 0
|
||||
|
||||
@@ -4,15 +4,15 @@ use serde_json::json;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
static IS_LISTENING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return Err("Device is already listening".to_string());
|
||||
if IS_LISTENING.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
IS_LISTENING.store(true, Ordering::SeqCst);
|
||||
|
||||
let callback = move |event: Event| {
|
||||
let device_event = match event.event_type {
|
||||
|
||||
@@ -244,7 +244,7 @@ fn build_device_event(event: &Event) -> Option<DeviceEvent> {
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return Err("Device is already listening".to_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
|
||||
61
src-tauri/src/core/gamepad.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use gilrs::{EventType, Gilrs};
|
||||
use serde::Serialize;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
static IS_LISTENING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum GamepadEventKind {
|
||||
ButtonChanged,
|
||||
AxisChanged,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct GamepadEvent {
|
||||
kind: GamepadEventKind,
|
||||
name: String,
|
||||
value: f32,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn start_gamepad_listing<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_LISTENING.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
IS_LISTENING.store(true, Ordering::SeqCst);
|
||||
|
||||
let mut gilrs = Gilrs::new().map_err(|err| err.to_string())?;
|
||||
|
||||
while IS_LISTENING.load(Ordering::SeqCst) {
|
||||
while let Some(event) = gilrs.next_event() {
|
||||
let gamepad_event = match event.event {
|
||||
EventType::ButtonChanged(button, value, ..) => GamepadEvent {
|
||||
kind: GamepadEventKind::ButtonChanged,
|
||||
name: format!("{:?}", button),
|
||||
value,
|
||||
},
|
||||
EventType::AxisChanged(axis, value, ..) => GamepadEvent {
|
||||
kind: GamepadEventKind::AxisChanged,
|
||||
name: format!("{:?}", axis),
|
||||
value,
|
||||
},
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let _ = app_handle.emit("gamepad-changed", gamepad_event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn stop_gamepad_listing() {
|
||||
if !IS_LISTENING.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
IS_LISTENING.store(false, Ordering::SeqCst);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod device;
|
||||
pub mod gamepad;
|
||||
pub mod prevent_default;
|
||||
pub mod setup;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
mod core;
|
||||
mod utils;
|
||||
|
||||
use core::{device::start_device_listening, prevent_default, setup};
|
||||
use core::{
|
||||
device::start_device_listening,
|
||||
gamepad::{start_gamepad_listing, stop_gamepad_listing},
|
||||
prevent_default, setup,
|
||||
};
|
||||
use tauri::{Manager, WindowEvent, generate_handler};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_custom_window::{
|
||||
@@ -23,7 +27,12 @@ pub fn run() {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(generate_handler![copy_dir, start_device_listening])
|
||||
.invoke_handler(generate_handler![
|
||||
copy_dir,
|
||||
start_device_listening,
|
||||
start_gamepad_listing,
|
||||
stop_gamepad_listing
|
||||
])
|
||||
.plugin(tauri_plugin_custom_window::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
@@ -36,7 +45,12 @@ pub fn run() {
|
||||
show_preference_window(app_handle);
|
||||
},
|
||||
))
|
||||
.plugin(tauri_plugin_log::Builder::new().build())
|
||||
.plugin(
|
||||
tauri_plugin_log::Builder::new()
|
||||
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
|
||||
.filter(|metadata| !metadata.target().contains("gilrs"))
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
|
||||
@@ -127,7 +127,10 @@ async function handleOk() {
|
||||
} catch (error) {
|
||||
message.error(String(error))
|
||||
} finally {
|
||||
state.downloading = false
|
||||
Object.assign(state, {
|
||||
downloading: false,
|
||||
downloadProgress: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isEqual, mapValues } from 'es-toolkit'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { readDir } from '@tauri-apps/plugin-fs'
|
||||
import { uniq } from 'es-toolkit'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
|
||||
import { LISTEN_KEY } from '../constants'
|
||||
import { INVOKE_KEY, LISTEN_KEY } from '../constants'
|
||||
|
||||
import { useModel } from './useModel'
|
||||
import { useTauriListen } from './useTauriListen'
|
||||
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
import { isImage } from '@/utils/is'
|
||||
import { join } from '@/utils/path'
|
||||
import { isWindows } from '@/utils/platform'
|
||||
|
||||
interface MouseButtonEvent {
|
||||
@@ -19,7 +15,7 @@ interface MouseButtonEvent {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface MouseMoveValue {
|
||||
interface MouseMoveValue {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
@@ -37,102 +33,41 @@ interface KeyboardEvent {
|
||||
type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent
|
||||
|
||||
export function useDevice() {
|
||||
const supportLeftKeys = ref<string[]>([])
|
||||
const supportRightKeys = ref<string[]>([])
|
||||
const pressedMouses = ref<string[]>([])
|
||||
const mousePosition = reactive<MouseMoveValue>({ x: 0, y: 0 })
|
||||
const pressedLeftKeys = ref<string[]>([])
|
||||
const pressedRightKeys = ref<string[]>([])
|
||||
const catStore = useCatStore()
|
||||
const modelStore = useModelStore()
|
||||
const lastMousePoint = ref<MouseMoveValue>({ x: 0, y: 0 })
|
||||
const releaseTimers = new Map<string, NodeJS.Timeout>()
|
||||
const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel()
|
||||
|
||||
watch(() => modelStore.currentModel, async (model) => {
|
||||
if (!model) return
|
||||
|
||||
const keySides = [
|
||||
{
|
||||
side: 'left',
|
||||
supportKeys: supportLeftKeys,
|
||||
pressedKeys: pressedLeftKeys,
|
||||
},
|
||||
{
|
||||
side: 'right',
|
||||
supportKeys: supportRightKeys,
|
||||
pressedKeys: pressedRightKeys,
|
||||
},
|
||||
]
|
||||
|
||||
for await (const item of keySides) {
|
||||
const { side, supportKeys, pressedKeys } = item
|
||||
|
||||
try {
|
||||
const files = await readDir(join(model.path, 'resources', `${side}-keys`))
|
||||
|
||||
const imageFiles = files.filter(file => isImage(file.name))
|
||||
|
||||
supportKeys.value = imageFiles.map((item) => {
|
||||
return item.name.split('.')[0]
|
||||
})
|
||||
|
||||
pressedKeys.value = pressedKeys.value.filter((key) => {
|
||||
return supportKeys.value.includes(key)
|
||||
})
|
||||
} catch {
|
||||
supportKeys.value = []
|
||||
pressedKeys.value = []
|
||||
}
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
const handlePress = (array: Ref<string[]>, value?: string) => {
|
||||
if (!value) return
|
||||
|
||||
if (catStore.singleMode) {
|
||||
array.value = [value]
|
||||
} else {
|
||||
array.value = uniq(array.value.concat(value))
|
||||
}
|
||||
}
|
||||
|
||||
const handleRelease = (array: Ref<string[]>, value?: string) => {
|
||||
if (!value) return
|
||||
|
||||
array.value = array.value.filter(item => item !== value)
|
||||
const startListening = () => {
|
||||
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
|
||||
}
|
||||
|
||||
const getSupportedKey = (key: string) => {
|
||||
for (const side of ['left', 'right']) {
|
||||
let nextKey = key
|
||||
let nextKey = key
|
||||
|
||||
const supportKeys = side === 'left' ? supportLeftKeys.value : supportRightKeys.value
|
||||
const unsupportedKey = !modelStore.supportKeys[nextKey]
|
||||
|
||||
const unsupportedKeys = !supportKeys.includes(key)
|
||||
|
||||
if (key.startsWith('F') && unsupportedKeys) {
|
||||
nextKey = key.replace(/F(\d+)/, 'Fn')
|
||||
}
|
||||
|
||||
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
|
||||
if (key.startsWith(item) && unsupportedKeys) {
|
||||
const regex = new RegExp(`^(${item}).*`)
|
||||
nextKey = key.replace(regex, '$1')
|
||||
}
|
||||
}
|
||||
|
||||
if (!supportKeys.includes(nextKey)) continue
|
||||
|
||||
return nextKey
|
||||
if (key.startsWith('F') && unsupportedKey) {
|
||||
nextKey = key.replace(/F(\d+)/, 'Fn')
|
||||
}
|
||||
|
||||
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
|
||||
if (key.startsWith(item) && unsupportedKey) {
|
||||
const regex = new RegExp(`^(${item}).*`)
|
||||
nextKey = key.replace(regex, '$1')
|
||||
}
|
||||
}
|
||||
|
||||
return nextKey
|
||||
}
|
||||
|
||||
const handleScheduleRelease = (keys: Ref<string[]>, key: string, delay = 500) => {
|
||||
const handleScheduleRelease = (key: string, delay = 500) => {
|
||||
if (releaseTimers.has(key)) {
|
||||
clearTimeout(releaseTimers.get(key))
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
handleRelease(keys, key)
|
||||
handleRelease(key)
|
||||
|
||||
releaseTimers.delete(key)
|
||||
}, delay)
|
||||
@@ -140,6 +75,16 @@ export function useDevice() {
|
||||
releaseTimers.set(key, timer)
|
||||
}
|
||||
|
||||
const processMouseMove = (value: MouseMoveValue) => {
|
||||
const roundedValue = mapValues(value, Math.round)
|
||||
|
||||
if (isEqual(lastMousePoint.value, roundedValue)) return
|
||||
|
||||
lastMousePoint.value = roundedValue
|
||||
|
||||
return handleMouseMove()
|
||||
}
|
||||
|
||||
useTauriListen<DeviceEvent>(LISTEN_KEY.DEVICE_CHANGED, ({ payload }) => {
|
||||
const { kind, value } = payload
|
||||
|
||||
@@ -148,41 +93,34 @@ export function useDevice() {
|
||||
|
||||
if (!nextValue) return
|
||||
|
||||
const isLeftSide = supportLeftKeys.value.includes(nextValue)
|
||||
|
||||
const pressedKeys = isLeftSide ? pressedLeftKeys : pressedRightKeys
|
||||
|
||||
if (nextValue === 'CapsLock') {
|
||||
handlePress(pressedKeys, nextValue)
|
||||
handlePress(nextValue)
|
||||
|
||||
return handleScheduleRelease(pressedKeys, nextValue, 100)
|
||||
return handleScheduleRelease(nextValue, 100)
|
||||
}
|
||||
|
||||
if (kind === 'KeyboardPress') {
|
||||
if (isWindows) {
|
||||
handleScheduleRelease(pressedKeys, nextValue)
|
||||
handleScheduleRelease(nextValue)
|
||||
}
|
||||
|
||||
return handlePress(pressedKeys, nextValue)
|
||||
return handlePress(nextValue)
|
||||
}
|
||||
|
||||
return handleRelease(pressedKeys, nextValue)
|
||||
return handleRelease(nextValue)
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case 'MousePress':
|
||||
return handlePress(pressedMouses, value)
|
||||
return handleMouseChange(value)
|
||||
case 'MouseRelease':
|
||||
return handleRelease(pressedMouses, value)
|
||||
return handleMouseChange(value, false)
|
||||
case 'MouseMove':
|
||||
return Object.assign(mousePosition, value)
|
||||
return processMouseMove(value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
pressedMouses,
|
||||
mousePosition,
|
||||
pressedLeftKeys,
|
||||
pressedRightKeys,
|
||||
startListening,
|
||||
}
|
||||
}
|
||||
|
||||
104
src/composables/useGamepad.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { LiteralUnion } from 'ant-design-vue/es/_util/type'
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
import { useModel } from './useModel'
|
||||
import { useTauriListen } from './useTauriListen'
|
||||
|
||||
import { INVOKE_KEY, LISTEN_KEY } from '@/constants'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
import live2d from '@/utils/live2d'
|
||||
|
||||
type GamepadEventName = LiteralUnion<'LeftStickX' | 'LeftStickY' | 'RightStickX' | 'RightStickY' | 'LeftThumb' | 'RightThumb'>
|
||||
|
||||
interface GamepadEvent {
|
||||
kind: 'ButtonChanged' | 'AxisChanged'
|
||||
name: GamepadEventName
|
||||
value: number
|
||||
}
|
||||
|
||||
interface StickState {
|
||||
x: number
|
||||
y: number
|
||||
moved: boolean
|
||||
pressed: boolean
|
||||
}
|
||||
|
||||
interface Sticks {
|
||||
left: StickState
|
||||
right: StickState
|
||||
}
|
||||
|
||||
const INITIAL_STICK_STATE: StickState = { x: 0, y: 0, moved: false, pressed: false }
|
||||
|
||||
export function useGamepad() {
|
||||
const { currentModel } = useModelStore()
|
||||
const { handlePress, handleRelease, handleAxisChange } = useModel()
|
||||
const sticks = reactive<Sticks>({
|
||||
left: { ...INITIAL_STICK_STATE },
|
||||
right: { ...INITIAL_STICK_STATE },
|
||||
})
|
||||
|
||||
const stickActive = computed(() => ({
|
||||
left: sticks.left.moved || sticks.left.pressed,
|
||||
right: sticks.right.moved || sticks.right.pressed,
|
||||
}))
|
||||
|
||||
watch(() => currentModel?.mode, (mode) => {
|
||||
if (mode === 'gamepad') {
|
||||
return invoke(INVOKE_KEY.START_GAMEPAD_LISTING)
|
||||
}
|
||||
|
||||
invoke(INVOKE_KEY.STOP_GAMEPAD_LISTING)
|
||||
}, { immediate: true })
|
||||
|
||||
watch(sticks.left, ({ x, y, moved, pressed }) => {
|
||||
sticks.left.moved = x !== 0 || y !== 0
|
||||
|
||||
live2d.setParameterValue('CatParamStickShowLeftHand', moved || pressed)
|
||||
}, { deep: true })
|
||||
|
||||
watch(sticks.right, ({ x, y, moved, pressed }) => {
|
||||
sticks.right.moved = x !== 0 || y !== 0
|
||||
|
||||
live2d.setParameterValue('CatParamStickShowRightHand', moved || pressed)
|
||||
}, { deep: true })
|
||||
|
||||
useTauriListen<GamepadEvent>(LISTEN_KEY.GAMEPAD_CHANGED, ({ payload }) => {
|
||||
const { name, value } = payload
|
||||
|
||||
switch (name) {
|
||||
case 'LeftStickX':
|
||||
sticks.left.x = value
|
||||
|
||||
return handleAxisChange('CatParamStickLX', value)
|
||||
case 'LeftStickY':
|
||||
sticks.left.y = value
|
||||
|
||||
return handleAxisChange('CatParamStickLY', value)
|
||||
case 'RightStickX':
|
||||
sticks.right.x = value
|
||||
|
||||
return handleAxisChange('CatParamStickRX', value)
|
||||
case 'RightStickY':
|
||||
sticks.right.y = value
|
||||
|
||||
return handleAxisChange('CatParamStickRY', value)
|
||||
case 'LeftThumb':
|
||||
sticks.left.pressed = value !== 0
|
||||
|
||||
return live2d.setParameterValue('CatParamStickLeftDown', value !== 0)
|
||||
case 'RightThumb':
|
||||
sticks.right.pressed = value !== 0
|
||||
|
||||
return live2d.setParameterValue('CatParamStickRightDown', value !== 0)
|
||||
default:
|
||||
return value > 0 ? handlePress(name) : handleRelease(name)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
stickActive,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
import type { MouseMoveValue } from './useDevice.ts'
|
||||
import type { Monitor } from '@tauri-apps/api/window'
|
||||
|
||||
import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'
|
||||
import { resolveResource } from '@tauri-apps/api/path'
|
||||
import { LogicalSize } from '@tauri-apps/api/dpi'
|
||||
import { resolveResource, sep } from '@tauri-apps/api/path'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { availableMonitors as getAvailableMonitors } from '@tauri-apps/api/window'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { isNil, round } from 'es-toolkit'
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue'
|
||||
import { nth } from 'es-toolkit/compat'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import live2d from '../utils/live2d'
|
||||
import { getCursorMonitor } from '../utils/monitor'
|
||||
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
import { getCursorMonitor } from '@/utils/monitor'
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
interface ModelSize {
|
||||
export interface ModelSize {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
@@ -26,28 +23,6 @@ export function useModel() {
|
||||
const modelStore = useModelStore()
|
||||
const catStore = useCatStore()
|
||||
const modelSize = ref<ModelSize>()
|
||||
const availableMonitors = ref<Monitor[]>([])
|
||||
|
||||
const isOnlySingleMonitor = computed(() => availableMonitors.value.length === 1)
|
||||
|
||||
onBeforeMount(async () => {
|
||||
availableMonitors.value = await getAvailableMonitors()
|
||||
})
|
||||
|
||||
watch(() => modelStore.currentModel, handleLoad, { deep: true, immediate: true })
|
||||
|
||||
watch([() => catStore.scale, modelSize], async () => {
|
||||
if (!modelSize.value) return
|
||||
|
||||
const { width, height } = modelSize.value
|
||||
|
||||
appWindow.setSize(
|
||||
new PhysicalSize({
|
||||
width: round(width * (catStore.scale / 100)),
|
||||
height: round(height * (catStore.scale / 100)),
|
||||
}),
|
||||
)
|
||||
}, { immediate: true })
|
||||
|
||||
async function handleLoad() {
|
||||
try {
|
||||
@@ -76,7 +51,7 @@ export function useModel() {
|
||||
async function handleResize() {
|
||||
if (!modelSize.value) return
|
||||
|
||||
live2d.fitModel()
|
||||
live2d.resizeModel(modelSize.value)
|
||||
|
||||
const { width, height } = modelSize.value
|
||||
|
||||
@@ -94,29 +69,51 @@ export function useModel() {
|
||||
catStore.scale = round((size.width / width) * 100)
|
||||
}
|
||||
|
||||
function handleKeyDown(side: 'left' | 'right', pressed: boolean) {
|
||||
const id = side === 'left' ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'
|
||||
const handlePress = (key: string) => {
|
||||
const path = modelStore.supportKeys[key]
|
||||
|
||||
const { min, max } = live2d.getParameterRange(id)
|
||||
if (!path) return
|
||||
|
||||
live2d.setParameterValue(id, pressed ? max : min)
|
||||
if (catStore.singleMode) {
|
||||
const dirName = nth(path.split(sep()), -2)!
|
||||
|
||||
const filterKeys = Object.entries(modelStore.pressedKeys).filter(([, value]) => {
|
||||
return value.includes(dirName)
|
||||
})
|
||||
|
||||
for (const [key] of filterKeys) {
|
||||
handleRelease(key)
|
||||
}
|
||||
}
|
||||
|
||||
modelStore.pressedKeys[key] = path
|
||||
}
|
||||
|
||||
async function _getCursorMonitor(mousePosition: MouseMoveValue) {
|
||||
return isOnlySingleMonitor.value
|
||||
? { ...availableMonitors.value[0], cursorPosition: mousePosition }
|
||||
: await getCursorMonitor()
|
||||
const handleRelease = (key: string) => {
|
||||
delete modelStore.pressedKeys[key]
|
||||
}
|
||||
|
||||
async function handleMouseMove(mousePosition: MouseMoveValue) {
|
||||
const monitor = await _getCursorMonitor(mousePosition)
|
||||
function handleKeyChange(isLeft = true, pressed = true) {
|
||||
const id = isLeft ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'
|
||||
|
||||
live2d.setParameterValue(id, pressed)
|
||||
}
|
||||
|
||||
function handleMouseChange(key: string, pressed = true) {
|
||||
const id = key === 'Left' ? 'ParamMouseLeftDown' : 'ParamMouseRightDown'
|
||||
|
||||
live2d.setParameterValue(id, pressed)
|
||||
}
|
||||
|
||||
async function handleMouseMove() {
|
||||
const monitor = await getCursorMonitor()
|
||||
|
||||
if (!monitor) return
|
||||
|
||||
const { size, position, cursorPosition } = monitor
|
||||
const { size, position, cursorPoint } = monitor
|
||||
|
||||
const xRatio = (cursorPosition.x - position.x) / size.width
|
||||
const yRatio = (cursorPosition.y - position.y) / size.height
|
||||
const xRatio = (cursorPoint.x - position.x) / size.width
|
||||
const yRatio = (cursorPoint.y - position.y) / size.height
|
||||
|
||||
for (const id of ['ParamMouseX', 'ParamMouseY', 'ParamAngleX', 'ParamAngleY']) {
|
||||
const { min, max } = live2d.getParameterRange(id)
|
||||
@@ -136,25 +133,22 @@ export function useModel() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(value: string[]) {
|
||||
const params = {
|
||||
ParamMouseLeftDown: value.includes('Left'),
|
||||
ParamMouseRightDown: value.includes('Right'),
|
||||
}
|
||||
async function handleAxisChange(id: string, value: number) {
|
||||
const { min, max } = live2d.getParameterRange(id)
|
||||
|
||||
for (const [id, pressed] of Object.entries(params)) {
|
||||
const { min, max } = live2d.getParameterRange(id)
|
||||
|
||||
live2d.setParameterValue(id, pressed ? max : min)
|
||||
}
|
||||
live2d.setParameterValue(id, Math.max(min, value * max))
|
||||
}
|
||||
|
||||
return {
|
||||
modelSize,
|
||||
handlePress,
|
||||
handleRelease,
|
||||
handleLoad,
|
||||
handleDestroy,
|
||||
handleResize,
|
||||
handleKeyDown,
|
||||
handleKeyChange,
|
||||
handleMouseChange,
|
||||
handleMouseMove,
|
||||
handleMouseDown,
|
||||
handleAxisChange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@tauri-apps/plugin-global-shortcut'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
export function useTauriKeyPress(shortcut: Ref<string, string>, callback: ShortcutHandler) {
|
||||
export function useTauriShortcut(shortcut: Ref<string, string>, callback: ShortcutHandler) {
|
||||
const oldShortcut = ref(shortcut.value)
|
||||
|
||||
watch(shortcut, async (value) => {
|
||||
@@ -7,9 +7,12 @@ export const LISTEN_KEY = {
|
||||
HIDE_WINDOW: 'hide-window',
|
||||
DEVICE_CHANGED: 'device-changed',
|
||||
UPDATE_APP: 'update-app',
|
||||
GAMEPAD_CHANGED: 'gamepad-changed',
|
||||
}
|
||||
|
||||
export const INVOKE_KEY = {
|
||||
COPY_DIR: 'copy_dir',
|
||||
START_DEVICE_LISTENING: 'start_device_listening',
|
||||
START_GAMEPAD_LISTING: 'start_gamepad_listing',
|
||||
STOP_GAMEPAD_LISTING: 'stop_gamepad_listing',
|
||||
}
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { MouseMoveValue } from '@/composables/useDevice'
|
||||
|
||||
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { PhysicalSize } from '@tauri-apps/api/dpi'
|
||||
import { Menu } from '@tauri-apps/api/menu'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { exists } from '@tauri-apps/plugin-fs'
|
||||
import { useDebounceFn, useEventListener, useRafFn } from '@vueuse/core'
|
||||
import { isEqual } from 'es-toolkit'
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core'
|
||||
import { nth } from 'es-toolkit/compat'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useDevice } from '@/composables/useDevice'
|
||||
import { useGamepad } from '@/composables/useGamepad'
|
||||
import { useModel } from '@/composables/useModel'
|
||||
import { useSharedMenu } from '@/composables/useSharedMenu'
|
||||
import { INVOKE_KEY } from '@/constants'
|
||||
import { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/plugins/window'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useGeneralStore } from '@/stores/general.ts'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
import { join } from '@/utils/path'
|
||||
|
||||
const { startListening } = useDevice()
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
const { pressedMouses, mousePosition, pressedLeftKeys, pressedRightKeys } = useDevice()
|
||||
const { handleDestroy, handleResize, handleMouseDown, handleMouseMove, handleKeyDown } = useModel()
|
||||
const { modelSize, handleLoad, handleDestroy, handleResize, handleKeyChange } = useModel()
|
||||
const catStore = useCatStore()
|
||||
const { getSharedMenu } = useSharedMenu()
|
||||
const modelStore = useModelStore()
|
||||
const generalStore = useGeneralStore()
|
||||
const resizing = ref(false)
|
||||
const backgroundImagePath = ref<string>()
|
||||
const { stickActive } = useGamepad()
|
||||
|
||||
onMounted(() => {
|
||||
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
|
||||
})
|
||||
onMounted(startListening)
|
||||
|
||||
onUnmounted(handleDestroy)
|
||||
|
||||
@@ -47,24 +46,42 @@ useEventListener('resize', () => {
|
||||
debouncedResize()
|
||||
})
|
||||
|
||||
watch(pressedMouses, handleMouseDown)
|
||||
watch(() => modelStore.currentModel, async (model) => {
|
||||
handleLoad()
|
||||
|
||||
useRafFn((() => {
|
||||
const cached: MouseMoveValue = { x: 0, y: 0 }
|
||||
return () => {
|
||||
if (isEqual(cached, mousePosition)) return
|
||||
Object.assign(cached, mousePosition)
|
||||
handleMouseMove(mousePosition)
|
||||
}
|
||||
})())
|
||||
if (!model) return
|
||||
|
||||
watch(pressedLeftKeys, (keys) => {
|
||||
handleKeyDown('left', keys.length > 0)
|
||||
})
|
||||
const path = join(model.path, 'resources', 'background.png')
|
||||
|
||||
watch(pressedRightKeys, (keys) => {
|
||||
handleKeyDown('right', keys.length > 0)
|
||||
})
|
||||
const existed = await exists(path)
|
||||
|
||||
backgroundImagePath.value = existed ? convertFileSrc(path) : void 0
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch([() => catStore.scale, modelSize], async () => {
|
||||
if (!modelSize.value) return
|
||||
|
||||
const { width, height } = modelSize.value
|
||||
|
||||
appWindow.setSize(
|
||||
new PhysicalSize({
|
||||
width: Math.round(width * (catStore.scale / 100)),
|
||||
height: Math.round(height * (catStore.scale / 100)),
|
||||
}),
|
||||
)
|
||||
}, { immediate: true })
|
||||
|
||||
watch([modelStore.pressedKeys, stickActive], ([keys, stickActive]) => {
|
||||
const dirs = Object.values(keys).map((path) => {
|
||||
return nth(path.split(sep()), -2)!
|
||||
})
|
||||
|
||||
const hasLeft = dirs.some(dir => dir.startsWith('left'))
|
||||
const hasRight = dirs.some(dir => dir.startsWith('right'))
|
||||
|
||||
handleKeyChange(true, stickActive.left || hasLeft)
|
||||
handleKeyChange(false, stickActive.right || hasRight)
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => catStore.visible, async (value) => {
|
||||
value ? showWindow() : hideWindow()
|
||||
@@ -76,16 +93,6 @@ watch(() => catStore.penetrable, (value) => {
|
||||
|
||||
watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true })
|
||||
|
||||
watch(() => modelStore.currentModel, async (model) => {
|
||||
if (!model) return
|
||||
|
||||
const path = join(model.path, 'resources', 'background.png')
|
||||
|
||||
const existed = await exists(path)
|
||||
|
||||
backgroundImagePath.value = existed ? convertFileSrc(path) : void 0
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(() => generalStore.taskbarVisibility, setTaskbarVisibility, { immediate: true })
|
||||
|
||||
function handleWindowDrag() {
|
||||
@@ -101,10 +108,6 @@ async function handleContextmenu(event: MouseEvent) {
|
||||
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
function resolveKeyImagePath(key: string, side: 'left' | 'right' = 'left') {
|
||||
return convertFileSrc(join(modelStore.currentModel!.path, 'resources', `${side}-keys`, `${key}.png`))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -123,15 +126,9 @@ function resolveKeyImagePath(key: string, side: 'left' | 'right' = 'left') {
|
||||
<canvas id="live2dCanvas" />
|
||||
|
||||
<img
|
||||
v-for="key in pressedLeftKeys"
|
||||
:key="key"
|
||||
:src="resolveKeyImagePath(key)"
|
||||
>
|
||||
|
||||
<img
|
||||
v-for="key in pressedRightKeys"
|
||||
:key="key"
|
||||
:src="resolveKeyImagePath(key, 'right')"
|
||||
v-for="path in modelStore.pressedKeys"
|
||||
:key="path"
|
||||
:src="convertFileSrc(path)"
|
||||
>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelMode } from '@/stores/model'
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { appDataDir } from '@tauri-apps/api/path'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
@@ -53,25 +55,35 @@ async function handleUpload() {
|
||||
}
|
||||
|
||||
watch(selectPaths, async (paths) => {
|
||||
for await (const path of paths) {
|
||||
for await (const fromPath of paths) {
|
||||
try {
|
||||
const id = nanoid()
|
||||
|
||||
const files = await readDir(join(path, 'resources'))
|
||||
let mode: ModelMode = 'standard'
|
||||
|
||||
const isKeyboardMode = files.some(file => file.name === 'right-keys')
|
||||
const files = await readDir(join(fromPath, 'resources', 'right-keys')).catch(() => [])
|
||||
|
||||
if (files.length > 0) {
|
||||
const fileNames = files.map(file => file.name.split('.')[0])
|
||||
|
||||
if (fileNames.includes('East')) {
|
||||
mode = 'gamepad'
|
||||
} else {
|
||||
mode = 'keyboard'
|
||||
}
|
||||
}
|
||||
|
||||
const toPath = join(await appDataDir(), 'custom-models', id)
|
||||
|
||||
await invoke(INVOKE_KEY.COPY_DIR, {
|
||||
fromPath: path,
|
||||
fromPath,
|
||||
toPath,
|
||||
})
|
||||
|
||||
modelStore.models.push({
|
||||
id,
|
||||
path: toPath,
|
||||
mode: isKeyboardMode ? 'keyboard' : 'standard',
|
||||
mode,
|
||||
isPreset: false,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia'
|
||||
|
||||
import ProList from '@/components/pro-list/index.vue'
|
||||
import ProShortcut from '@/components/pro-shortcut/index.vue'
|
||||
import { useTauriKeyPress } from '@/composables/useTauriKeyPress'
|
||||
import { useTauriShortcut } from '@/composables/useTauriShortcut'
|
||||
import { toggleWindowVisible } from '@/plugins/window'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useShortcutStore } from '@/stores/shortcut.ts'
|
||||
@@ -12,23 +12,23 @@ const shortcutStore = useShortcutStore()
|
||||
const { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = storeToRefs(shortcutStore)
|
||||
const catStore = useCatStore()
|
||||
|
||||
useTauriKeyPress(visibleCat, () => {
|
||||
useTauriShortcut(visibleCat, () => {
|
||||
catStore.visible = !catStore.visible
|
||||
})
|
||||
|
||||
useTauriKeyPress(visiblePreference, () => {
|
||||
useTauriShortcut(visiblePreference, () => {
|
||||
toggleWindowVisible('preference')
|
||||
})
|
||||
|
||||
useTauriKeyPress(mirrorMode, () => {
|
||||
useTauriShortcut(mirrorMode, () => {
|
||||
catStore.mirrorMode = !catStore.mirrorMode
|
||||
})
|
||||
|
||||
useTauriKeyPress(penetrable, () => {
|
||||
useTauriShortcut(penetrable, () => {
|
||||
catStore.penetrable = !catStore.penetrable
|
||||
})
|
||||
|
||||
useTauriKeyPress(alwaysOnTop, () => {
|
||||
useTauriShortcut(alwaysOnTop, () => {
|
||||
catStore.alwaysOnTop = !catStore.alwaysOnTop
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { resolveResource } from '@tauri-apps/api/path'
|
||||
import { readDir } from '@tauri-apps/plugin-fs'
|
||||
import { filter, find } from 'es-toolkit/compat'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
|
||||
import { isImage } from '@/utils/is'
|
||||
import { join } from '@/utils/path'
|
||||
import { clearObject } from '@/utils/shared'
|
||||
|
||||
export type ModelMode = 'standard' | 'keyboard' | 'handle'
|
||||
export type ModelMode = 'standard' | 'keyboard' | 'gamepad'
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
@@ -36,48 +40,68 @@ export const useModelStore = defineStore('model', () => {
|
||||
const currentModel = ref<Model>()
|
||||
const motions = ref<MotionGroup>({})
|
||||
const expressions = ref<Expression[]>([])
|
||||
const supportKeys = reactive<Record<string, string>>({})
|
||||
const pressedKeys = reactive<Record<string, string>>({})
|
||||
|
||||
const init = async () => {
|
||||
const presetModelsPath = await resolveResource('assets/models')
|
||||
const modelsPath = await resolveResource('assets/models')
|
||||
|
||||
if (models.value.length === 0) {
|
||||
const modes: ModelMode[] = ['standard', 'keyboard']
|
||||
const nextModels = filter(models.value, { isPreset: false })
|
||||
const presetModels = filter(models.value, { isPreset: true })
|
||||
|
||||
for (const mode of modes) {
|
||||
models.value.push({
|
||||
mode,
|
||||
id: nanoid(),
|
||||
isPreset: true,
|
||||
path: join(presetModelsPath, mode),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
models.value = models.value.map((item) => {
|
||||
const { isPreset, mode } = item
|
||||
const modes: ModelMode[] = ['gamepad', 'keyboard', 'standard']
|
||||
|
||||
if (!isPreset) return item
|
||||
for (const mode of modes) {
|
||||
const matched = find(presetModels, { mode })
|
||||
|
||||
return {
|
||||
...item,
|
||||
path: join(presetModelsPath, mode),
|
||||
}
|
||||
nextModels.unshift({
|
||||
id: matched?.id ?? nanoid(),
|
||||
mode,
|
||||
isPreset: true,
|
||||
path: join(modelsPath, mode),
|
||||
})
|
||||
}
|
||||
|
||||
const matched = models.value.find(item => item.id === currentModel.value?.id)
|
||||
const matched = find(nextModels, { id: currentModel.value?.id })
|
||||
|
||||
if (matched) {
|
||||
return currentModel.value = matched
|
||||
}
|
||||
currentModel.value = matched ?? nextModels[0]
|
||||
|
||||
currentModel.value = models.value[0]
|
||||
models.value = nextModels
|
||||
}
|
||||
|
||||
watch(currentModel, async (model) => {
|
||||
if (!model) return
|
||||
|
||||
clearObject([supportKeys, pressedKeys])
|
||||
|
||||
const resourcePath = join(model.path, 'resources')
|
||||
const groups = ['left-keys', 'right-keys']
|
||||
|
||||
for await (const groupName of groups) {
|
||||
const groupDir = join(resourcePath, groupName)
|
||||
const files = await readDir(groupDir).catch(() => [])
|
||||
const imageFiles = files.filter(file => isImage(file.name))
|
||||
|
||||
for (const file of imageFiles) {
|
||||
const fileName = file.name.split('.')[0]
|
||||
|
||||
supportKeys[fileName] = join(groupDir, file.name)
|
||||
}
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
return {
|
||||
models,
|
||||
currentModel,
|
||||
motions,
|
||||
expressions,
|
||||
supportKeys,
|
||||
pressedKeys,
|
||||
init,
|
||||
}
|
||||
}, {
|
||||
tauri: {
|
||||
filterKeys: ['models', 'currentModel'],
|
||||
filterKeysStrategy: 'pick',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ModelSize } from '@/composables/useModel'
|
||||
import type { Cubism4InternalModel } from 'pixi-live2d-display'
|
||||
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
@@ -12,8 +13,6 @@ Live2DModel.registerTicker(Ticker)
|
||||
class Live2d {
|
||||
private app: Application | null = null
|
||||
public model: Live2DModel | null = null
|
||||
private modelWidth = 0
|
||||
private modelHeight = 0
|
||||
|
||||
constructor() { }
|
||||
|
||||
@@ -58,12 +57,9 @@ class Live2d {
|
||||
|
||||
this.model = await Live2DModel.from(modelSettings)
|
||||
|
||||
const { width, height } = this.model
|
||||
this.modelWidth = width
|
||||
this.modelHeight = height
|
||||
|
||||
this.app?.stage.addChild(this.model)
|
||||
|
||||
const { width, height } = this.model
|
||||
const { motions, expressions } = modelSettings
|
||||
|
||||
return {
|
||||
@@ -78,11 +74,13 @@ class Live2d {
|
||||
this.model?.destroy()
|
||||
}
|
||||
|
||||
public fitModel() {
|
||||
public resizeModel(modelSize: ModelSize) {
|
||||
if (!this.model) return
|
||||
|
||||
const scaleX = innerWidth / this.modelWidth
|
||||
const scaleY = innerHeight / this.modelHeight
|
||||
const { width, height } = modelSize
|
||||
|
||||
const scaleX = innerWidth / width
|
||||
const scaleY = innerHeight / height
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
this.model.scale.set(scale)
|
||||
@@ -118,7 +116,7 @@ class Live2d {
|
||||
}
|
||||
}
|
||||
|
||||
public setParameterValue(id: string, value: number) {
|
||||
public setParameterValue(id: string, value: number | boolean) {
|
||||
const coreModel = this.getCoreModel()
|
||||
|
||||
return coreModel?.setParameterValueById?.(id, Number(value))
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import {
|
||||
cursorPosition,
|
||||
monitorFromPoint,
|
||||
} from '@tauri-apps/api/window'
|
||||
import { cursorPosition, monitorFromPoint } from '@tauri-apps/api/window'
|
||||
|
||||
export async function getCursorMonitor() {
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
const scaleFactor = await appWindow.scaleFactor()
|
||||
|
||||
const point = await cursorPosition()
|
||||
const cursorPoint = await cursorPosition()
|
||||
|
||||
const { x, y } = point.toLogical(scaleFactor)
|
||||
const { x, y } = cursorPoint.toLogical(scaleFactor)
|
||||
|
||||
const monitor = await monitorFromPoint(x, y)
|
||||
|
||||
@@ -19,6 +16,6 @@ export async function getCursorMonitor() {
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
cursorPosition: point,
|
||||
cursorPoint,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { LiteralUnion } from 'ant-design-vue/es/_util/type'
|
||||
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
|
||||
export function join(...paths: LiteralUnion<'resources' | 'left-keys' | 'right-keys' | 'background.png' | 'cover.png'>[]) {
|
||||
const joinPaths = paths.map((path) => {
|
||||
if (path.endsWith(sep())) {
|
||||
return path.slice(0, -1)
|
||||
export function join(...paths: string[]) {
|
||||
const joinPaths = paths.map((path, index) => {
|
||||
if (index === 0) {
|
||||
return path.replace(new RegExp(`${sep()}+$`), '')
|
||||
} else {
|
||||
return path.replace(new RegExp(`^${sep()}+|${sep()}+$`, 'g'), '')
|
||||
}
|
||||
|
||||
return path
|
||||
})
|
||||
|
||||
return joinPaths.join(sep())
|
||||
|
||||
9
src/utils/shared.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { castArray } from 'es-toolkit/compat'
|
||||
|
||||
export function clearObject<T extends Record<string, unknown>>(targets: T | T[]) {
|
||||
for (const target of castArray<T>(targets)) {
|
||||
for (const key of Object.keys(target)) {
|
||||
delete target[key]
|
||||
}
|
||||
}
|
||||
}
|
||||