index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. <template>
  2. <div>
  3. <Transition :enter-active-class="proxy?.animate.mask.enter">
  4. <div class="mask" v-if="actionState"></div>
  5. </Transition>
  6. <Transition :enter-active-class="proxy?.animate.rankingWindow.enter"
  7. :leave-active-class="proxy?.animate.rankingWindow.leave">
  8. <div class="confirmDiaBg" v-if="actionState">
  9. <div class="confirmDiaWindow">
  10. <div class="prompt"><i></i> {{ actionText }}</div>
  11. <div class="confirmDiaWindow-con">
  12. <div class="picBox">
  13. <div class="pic">
  14. <div class="gamePic">
  15. <template v-if="currentGame == 'game_fruit'">
  16. <img src="@/assets/images/game/img2.png" width="200" />
  17. </template>
  18. <template v-if="currentGame == 'game_basketball'">
  19. <img src="@/assets/images/game/img1.png" width="200" />
  20. </template>
  21. <template v-if="currentGame == 'game_football'">
  22. <img src="@/assets/images/game/img3.png" width="200" />
  23. </template>
  24. <div class="time" v-if="timerNum>0"><div>{{timerNum}}</div></div>
  25. </div>
  26. <div class="gamePlayer">
  27. <div class="item" v-for="item in areaStateList" :key="item.area">
  28. <div class="player">
  29. <template v-if="currentGame == 'bodyposecontroller'">
  30. <div><img src="@/assets/images/game/fruit1.png" class="fade-image"
  31. :class="{ 'fade-image1': !item.state }"></img></div>
  32. <div><img v-if="!item.state" src="@/assets/images/game/fruit2.png" class="fade-image"
  33. :class="{ 'fade-image2': !item.state }"></img></div>
  34. </template>
  35. <template v-if="currentGame == 'game_fruit'">
  36. <div><img src="@/assets/images/game/fruit1.png" class="fade-image"
  37. :class="{ 'fade-image1': !item.state }"></img></div>
  38. <div><img v-if="!item.state" src="@/assets/images/game/fruit2.png" class="fade-image"
  39. :class="{ 'fade-image2': !item.state }"></img></div>
  40. </template>
  41. <template v-if="currentGame == 'game_basketball'">
  42. <div><img src="@/assets/images/game/basketball1.png" class="fade-image"
  43. :class="{ 'fade-image1': !item.state }"></img></div>
  44. <div><img v-if="!item.state" src="@/assets/images/game/basketball2.png" class="fade-image"
  45. :class="{ 'fade-image2': !item.state }"></img></div>
  46. </template>
  47. <template v-if="currentGame == 'game_football'">
  48. <div><img src="@/assets/images/game/football1.png" class="fade-image"
  49. :class="{ 'fade-image1': !item.state }"></img></div>
  50. <div><img v-if="!item.state" src="@/assets/images/game/football2.png" class="fade-image"
  51. :class="{ 'fade-image2': !item.state }"></img></div>
  52. </template>
  53. <div v-if="item.state" class="ok"><img src="@/assets/images/game/ok.png"></div>
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. <div class="name">
  60. <template v-if="currentGame == 'game_fruit'">玩法:挥动双手切开水果,失败三次结束游戏</template>
  61. <template v-if="currentGame == 'game_basketball'">玩法:身体左右移动对准篮筐,双手投篮</template>
  62. <template v-if="currentGame == 'game_football'">玩法:身体左右移动躲避雪糕筒,踢腿射门</template>
  63. </div>
  64. </div>
  65. </div>
  66. <div @click="getExit" class="close"></div>
  67. </div>
  68. </Transition>
  69. </div>
  70. </template>
  71. <script setup lang="ts">
  72. import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
  73. const { proxy } = getCurrentInstance() as any;
  74. const emit = defineEmits(['confirmExit', 'confirmStart']);
  75. const data = reactive<any>({
  76. currentGame: "",
  77. currentGameArea: [],
  78. actionState: false,//窗口状态
  79. areaStateList: [],
  80. lock: false,
  81. actionText: "",
  82. timer: null,
  83. timerNum: null,
  84. });
  85. const { currentGame, currentGameArea, actionState, areaStateList, lock, actionText, timer, timerNum } = toRefs(data);
  86. onBeforeMount(() => {
  87. //初始化语音
  88. initSpeech();
  89. })
  90. onMounted(() => {
  91. })
  92. /**
  93. * 初始化
  94. */
  95. const getInit = (e: any) => {
  96. let area = e.ctrl_name.replace('bodyposecontroller_', '');
  97. let arr = e.data.result.boxes;
  98. let myArr = e.data.result.keypoints;
  99. let result = [];
  100. for (let i = 0; i < myArr.length; i += 3) {
  101. result.push(myArr.slice(i, i + 2));
  102. }
  103. //console.log("result", result)
  104. if (currentGame.value == 'bodyposecontroller') {
  105. let leftA = result[6][1];//右肩Y
  106. let rightA = result[5][1];//左肩Y
  107. let leftB = result[10][1];//右手Y
  108. let rightB = result[9][1];//左手Y
  109. let bizi = result[0][1];//鼻子Y
  110. if (leftB < leftA || rightB < rightA || leftB < bizi || rightB < bizi) {
  111. let myIndex = areaStateList.value.findIndex((item: any) => {
  112. return item.area == area;
  113. })
  114. let boxes = [{ x: arr[0], y: arr[3] }, { x: arr[0], y: arr[1] }, { x: arr[2], y: arr[1] }, { x: arr[2], y: arr[3] }];
  115. areaStateList.value[myIndex].state = true;
  116. areaStateList.value[myIndex].boxes = boxes;
  117. }
  118. }
  119. if (currentGame.value == 'game_basketball') {
  120. let leftA = result[6][1];//右肩Y
  121. let rightA = result[5][1];//左肩Y
  122. let leftB = result[10][1];//右手Y
  123. let rightB = result[9][1];//左手Y
  124. let bizi = result[0][1];//鼻子Y
  125. if (leftB < leftA || rightB < rightA || leftB < bizi || rightB < bizi) {
  126. let myIndex = areaStateList.value.findIndex((item: any) => {
  127. return item.area == area;
  128. })
  129. let boxes = [{ x: arr[0], y: arr[3] }, { x: arr[0], y: arr[1] }, { x: arr[2], y: arr[1] }, { x: arr[2], y: arr[3] }];
  130. areaStateList.value[myIndex].state = true;
  131. areaStateList.value[myIndex].boxes = boxes;
  132. }
  133. }
  134. if (currentGame.value == 'game_football') {
  135. let leftA = { x: result[12][0], y: result[12][1] };//大腿
  136. let leftB = { x: result[14][0], y: result[14][1] };//膝盖
  137. let leftC = { x: result[16][0], y: result[16][1] };//脚
  138. let rightA = { x: result[11][0], y: result[11][1] };//大腿
  139. let rightB = { x: result[13][0], y: result[13][1] };//膝盖
  140. let rightC = { x: result[15][0], y: result[15][1] };//脚
  141. let jiaodu1 = calculateAngleAtB(leftA, leftB, leftC)
  142. let jiaodu2 = calculateAngleAtB(rightA, rightB, rightC)
  143. // console.log("jiaodu1",jiaodu1)
  144. // console.log("jiaodu2",jiaodu2)
  145. if (jiaodu1 <= 80 && jiaodu2 >= 120 || jiaodu2 <= 80 && jiaodu2 >= 120) {
  146. let myIndex = areaStateList.value.findIndex((item: any) => {
  147. return item.area == area;
  148. })
  149. let boxes = [{ x: arr[0], y: arr[3] }, { x: arr[0], y: arr[1] }, { x: arr[2], y: arr[1] }, { x: arr[2], y: arr[3] }];
  150. areaStateList.value[myIndex].state = true;
  151. areaStateList.value[myIndex].boxes = boxes;
  152. }
  153. }
  154. if (currentGame.value == 'game_fruit') {
  155. let leftA = result[6][1];//右肩Y
  156. let rightA = result[5][1];//左肩Y
  157. let leftB = result[10][1];//右手Y
  158. let rightB = result[9][1];//左手Y
  159. let bizi = result[0][1];//鼻子Y
  160. if (leftB < leftA || rightB < rightA || leftB < bizi || rightB < bizi) {
  161. let myIndex = areaStateList.value.findIndex((item: any) => {
  162. return item.area == area;
  163. })
  164. let boxes = [{ x: arr[0], y: arr[3] }, { x: arr[0], y: arr[1] }, { x: arr[2], y: arr[1] }, { x: arr[2], y: arr[3] }];
  165. areaStateList.value[myIndex].state = true;
  166. areaStateList.value[myIndex].boxes = boxes;
  167. }
  168. }
  169. };
  170. /**
  171. * 计算B点的夹角度数(∠ABC)
  172. * @param {Object} pointA - A点坐标 {x, y, z可选}
  173. * @param {Object} pointB - B点坐标 {x, y, z可选}
  174. * @param {Object} pointC - C点坐标 {x, y, z可选}
  175. * @returns {number} B点的夹角度数(保留两位小数)
  176. */
  177. const calculateAngleAtB = (pointA, pointB, pointC) => {
  178. // 计算向量BA和向量BC
  179. const vectorBA = {
  180. x: pointA.x - pointB.x,
  181. y: pointA.y - pointB.y,
  182. z: (pointA.z || 0) - (pointB.z || 0)
  183. };
  184. const vectorBC = {
  185. x: pointC.x - pointB.x,
  186. y: pointC.y - pointB.y,
  187. z: (pointC.z || 0) - (pointB.z || 0)
  188. };
  189. // 计算点积
  190. const dotProduct =
  191. vectorBA.x * vectorBC.x +
  192. vectorBA.y * vectorBC.y +
  193. vectorBA.z * vectorBC.z;
  194. // 计算向量BA的模长
  195. const lengthBA = Math.sqrt(
  196. vectorBA.x ** 2 +
  197. vectorBA.y ** 2 +
  198. vectorBA.z ** 2
  199. );
  200. // 计算向量BC的模长
  201. const lengthBC = Math.sqrt(
  202. vectorBC.x ** 2 +
  203. vectorBC.y ** 2 +
  204. vectorBC.z ** 2
  205. );
  206. // 防止除以零的情况
  207. if (lengthBA === 0 || lengthBC === 0) {
  208. throw new Error("点A、B、C不能重合");
  209. }
  210. // 计算余弦值
  211. const cosine = dotProduct / (lengthBA * lengthBC);
  212. // 由于计算误差可能导致cosine略超出[-1, 1]范围,需要修正
  213. const clampedCosine = Math.max(Math.min(cosine, 1), -1);
  214. // 计算弧度并转换为角度
  215. const angleRadians = Math.acos(clampedCosine);
  216. const angleDegrees = angleRadians * (180 / Math.PI);
  217. // 保留两位小数并返回
  218. return parseFloat(angleDegrees.toFixed(2));
  219. }
  220. /**
  221. * 打开
  222. */
  223. const getOpen = async (game: any, area: any) => {
  224. lock.value = false;
  225. currentGame.value = game;
  226. currentGameArea.value = area;
  227. let list = area.map((item: any) => {
  228. let obj = {
  229. area: item,
  230. state: false
  231. }
  232. return obj;
  233. }) || [];
  234. areaStateList.value = list;
  235. actionState.value = true;
  236. let txt = ""
  237. if (currentGame.value == 'bodyposecontroller') {
  238. txt = "举手开始游戏";
  239. }
  240. if (currentGame.value == 'game_fruit') {
  241. txt = "举手开始游戏";
  242. }
  243. if (currentGame.value == 'game_basketball') {
  244. txt = "请做投篮动作开始游戏";
  245. }
  246. if (currentGame.value == 'game_football') {
  247. txt = "请踢腿开始游戏";
  248. }
  249. actionText.value = txt;
  250. speckText(actionText.value);
  251. };
  252. /**
  253. * 关闭
  254. */
  255. const getExit = () => {
  256. clearInterval(timer.value);
  257. timer.value = null;
  258. speckCancel(); //停止播报
  259. actionState.value = false;
  260. emit('confirmExit', {});
  261. };
  262. /**
  263. * 监听全部OK
  264. */
  265. watch(
  266. () => areaStateList.value,
  267. (newData, oldData) => {
  268. //都准备就绪就开始
  269. if (lock.value) {
  270. return false;
  271. }
  272. let state = newData.every((item: any) => {
  273. return item.state;
  274. })
  275. if (state) {
  276. lock.value = true;
  277. timerNum.value = 3;
  278. timer.value = setInterval(() => {
  279. if (timerNum.value == 1) {
  280. actionState.value = false;
  281. clearInterval(timer.value);
  282. timer.value = null;
  283. emit('confirmStart', areaStateList.value);
  284. }
  285. speckText(timerNum.value);
  286. timerNum.value--;
  287. }, 1000)
  288. }
  289. },
  290. { deep: true }
  291. );
  292. //暴露给父组件用
  293. defineExpose({
  294. getOpen,
  295. getInit
  296. })
  297. </script>
  298. <style lang="scss" scoped>
  299. .mask {
  300. position: fixed;
  301. height: 100vh;
  302. width: 100vw;
  303. top: 0;
  304. left: 0;
  305. background: rgba(0, 0, 0, 0.6);
  306. z-index: 998;
  307. }
  308. .confirmDiaBg {
  309. width: 70rem;
  310. position: fixed;
  311. left: 50%;
  312. top: 50%;
  313. margin-left: calc(70rem / -2);
  314. margin-top: calc(((70rem / 2) + 12rem) / -2);
  315. display: flex;
  316. flex-direction: column;
  317. z-index: 999;
  318. .confirmDiaWindow {
  319. border-radius: 1.6rem;
  320. opacity: 1;
  321. text-align: center;
  322. display: flex;
  323. align-items: center;
  324. justify-content: center;
  325. flex-direction: column;
  326. margin-bottom: 20px;
  327. background: linear-gradient(59deg, #092941 -85%, #2A484B 96%);
  328. box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.5577);
  329. .prompt {
  330. color: #ffffff;
  331. font-size: 2.5vh;
  332. display: flex;
  333. line-height: 3vh;
  334. padding-top: 2vh;
  335. i {
  336. width: 3vh;
  337. height: 3vh;
  338. margin-right: 3px;
  339. display: block;
  340. background-image: url('@/assets/images/game/prompt.png');
  341. background-position: bottom;
  342. background-repeat: no-repeat;
  343. background-size: 100%;
  344. }
  345. }
  346. .confirmDiaWindow-con {
  347. width: 85%;
  348. padding: 25px;
  349. display: flex;
  350. justify-content: center;
  351. flex-direction: column;
  352. .picBox {
  353. width: 100%;
  354. background-image: url('@/assets/images/game/bg.png');
  355. background-position: bottom;
  356. background-repeat: no-repeat;
  357. background-size: 100%;
  358. display: flex;
  359. justify-content: center;
  360. padding-bottom: 10vh;
  361. .pic {
  362. display: flex;
  363. justify-content: center;
  364. flex-direction: column;
  365. .gamePic {
  366. border: 5px solid #3BDDCE;
  367. width: 60vh;
  368. height: calc(60vh * (1080 / 1920));
  369. position: relative;
  370. display: flex;
  371. .time{
  372. position: absolute;
  373. left: 0;
  374. top: 0;
  375. width: 100%;
  376. height: 100%;
  377. background: rgba(0, 0, 0, 0.5);
  378. color: #ffffff;
  379. font-size: 7rem;
  380. display: flex;
  381. align-items: center;
  382. justify-content: center;
  383. }
  384. img {
  385. width: 100%;
  386. height: 100%;
  387. }
  388. }
  389. .gamePlayer {
  390. display: flex;
  391. width: 60vh;
  392. margin-top: -20vh;
  393. .item {
  394. flex: 1;
  395. display: flex;
  396. justify-content: center;
  397. .player {
  398. position: relative;
  399. width: 22vh;
  400. height: calc(22vh * (1000 / 518));
  401. }
  402. .fade-image {
  403. width: 100%;
  404. height: 100%;
  405. position: absolute;
  406. left: 0;
  407. top: 0;
  408. }
  409. .fade-image1 {
  410. animation: fadeOpacity1 2s infinite;
  411. }
  412. .fade-image2 {
  413. animation: fadeOpacity2 2s infinite;
  414. }
  415. .ok {
  416. width: 8vh;
  417. height: 8vh;
  418. position: absolute;
  419. top: 50%;
  420. left: 50%;
  421. margin-top: 1vh;
  422. margin-left: -4vh;
  423. img {
  424. width: 100%;
  425. }
  426. }
  427. @keyframes fadeOpacity1 {
  428. 0% {
  429. opacity: 1;
  430. }
  431. 100% {
  432. opacity: 0;
  433. }
  434. }
  435. @keyframes fadeOpacity2 {
  436. 0% {
  437. opacity: 0;
  438. }
  439. 100% {
  440. opacity: 1;
  441. }
  442. }
  443. }
  444. }
  445. }
  446. }
  447. .name {
  448. width: 100%;
  449. color: #ffffff;
  450. font-size: 2.8vh;
  451. padding-top: 20px;
  452. }
  453. }
  454. }
  455. .close {
  456. margin: 0 auto;
  457. }
  458. }
  459. </style>