林旭祥 hai 3 semanas
pai
achega
d7061e5c58

BIN=BIN
src/assets/images/game/basketball1.png


BIN=BIN
src/assets/images/game/basketball2.png


BIN=BIN
src/assets/images/game/bg.png


BIN=BIN
src/assets/images/game/football1.png


BIN=BIN
src/assets/images/game/football2.png


BIN=BIN
src/assets/images/game/fruit1.png


BIN=BIN
src/assets/images/game/fruit2.png


BIN=BIN
src/assets/images/game/img1.png


BIN=BIN
src/assets/images/game/img2.png


BIN=BIN
src/assets/images/game/img3.png


BIN=BIN
src/assets/images/game/ok.png


BIN=BIN
src/assets/images/game/prompt.png


+ 303 - 13
src/components/ActionConfirmWindow/index.vue

@@ -7,10 +7,56 @@
       :leave-active-class="proxy?.animate.rankingWindow.leave">
       <div class="confirmDiaBg" v-if="actionState">
         <div class="confirmDiaWindow">
+          <div class="prompt"><i></i> <template v-if="currentGame == 'game_fruit'">
+              举手开始游戏
+            </template>
+            <template v-if="currentGame == 'game_basketball'">
+              请做投篮动作开始游戏
+            </template>
+            <template v-if="currentGame == 'game_football'">
+              请踢腿开始游戏
+            </template>
+          </div>
+
           <div class="confirmDiaWindow-con">
-            <div class="pic">
-              <div v-for="item in areaStateList" :key="item.area">
-                {{ item.state }}
+            <div class="picBox">
+              <div class="pic">
+                <div class="gamePic">
+                  <template v-if="currentGame == 'game_fruit'">
+                    <img src="@/assets/images/game/img2.png" width="200" />
+                  </template>
+                  <template v-if="currentGame == 'game_basketball'">
+                    <img src="@/assets/images/game/img1.png" width="200" />
+                  </template>
+                  <template v-if="currentGame == 'game_football'">
+                    <img src="@/assets/images/game/img3.png" width="200" />
+                  </template>
+                </div>
+                <div class="gamePlayer">
+                  <div class="item" v-for="item in areaStateList" :key="item.area">
+                    <div class="player">
+                      <template v-if="currentGame == 'game_fruit'">
+                        <div><img src="@/assets/images/game/fruit1.png" class="fade-image"
+                            :class="{ 'fade-image1': !item.state }"></img></div>
+                        <div><img v-if="!item.state" src="@/assets/images/game/fruit2.png" class="fade-image"
+                            :class="{ 'fade-image2': !item.state }"></img></div>
+                      </template>
+                      <template v-if="currentGame == 'game_basketball'">
+                        <div><img src="@/assets/images/game/basketball1.png" class="fade-image"
+                            :class="{ 'fade-image1': !item.state }"></img></div>
+                        <div><img v-if="!item.state" src="@/assets/images/game/basketball2.png" class="fade-image"
+                            :class="{ 'fade-image2': !item.state }"></img></div>
+                      </template>
+                      <template v-if="currentGame == 'game_football'">
+                        <div><img src="@/assets/images/game/football1.png" class="fade-image"
+                            :class="{ 'fade-image1': !item.state }"></img></div>
+                        <div><img v-if="!item.state" src="@/assets/images/game/football2.png" class="fade-image"
+                            :class="{ 'fade-image2': !item.state }"></img></div>
+                      </template>
+                      <div v-if="item.state" class="ok"><img src="@/assets/images/game/ok.png"></div>
+                    </div>
+                  </div>
+                </div>
               </div>
             </div>
             <div class="name">
@@ -27,21 +73,25 @@
 
 </template>
 <script setup lang="ts">
+import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
 const { proxy } = getCurrentInstance() as any;
-const emit = defineEmits(['confirmExit']);
+const emit = defineEmits(['confirmExit', 'confirmStart']);
 
 const data = reactive<any>({
   currentGame: "",
   currentGameArea: [],
   actionState: false,//窗口状态
   areaStateList: [],
+  lock: false,
 });
 
-const { currentGame, currentGameArea, actionState, areaStateList } = toRefs(data);
+const { currentGame, currentGameArea, actionState, areaStateList, lock } = toRefs(data);
 
 
 
 onBeforeMount(() => {
+  //初始化语音
+  initSpeech();
 })
 
 onMounted(() => {
@@ -60,7 +110,12 @@ const getInit = (e: any) => {
   }
   //console.log("result", result)
   if (currentGame.value == 'bodyposecontroller') {
-
+    let myIndex = areaStateList.value.findIndex((item: any) => {
+      return item.area == area;
+    })
+    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] }];
+    areaStateList.value[myIndex].state = true;
+    areaStateList.value[myIndex].boxes = boxes;
   }
   if (currentGame.value == 'game_basketball') {
     let leftA = result[6][1];//右肩Y
@@ -78,18 +133,109 @@ const getInit = (e: any) => {
     }
   }
   if (currentGame.value == 'game_football') {
+    let leftA = { x: result[12][0], y: result[12][1] };//大腿
+    let leftB = { x: result[14][0], y: result[14][1] };//膝盖
+    let leftC = { x: result[16][0], y: result[16][1] };//脚
+
+    let rightA = { x: result[11][0], y: result[11][1] };//大腿
+    let rightB = { x: result[13][0], y: result[13][1] };//膝盖
+    let rightC = { x: result[15][0], y: result[15][1] };//脚
 
+    let jiaodu1 = calculateAngleAtB(leftA, leftB, leftC)
+    let jiaodu2 = calculateAngleAtB(rightA, rightB, rightC)
+    // console.log("jiaodu1",jiaodu1)
+    // console.log("jiaodu2",jiaodu2)
+    if (jiaodu1 <= 80 && jiaodu2 >= 120 || jiaodu2 <= 80 && jiaodu2 >= 120) {
+      let myIndex = areaStateList.value.findIndex((item: any) => {
+        return item.area == area;
+      })
+      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] }];
+      areaStateList.value[myIndex].state = true;
+      areaStateList.value[myIndex].boxes = boxes;
+    }
   }
   if (currentGame.value == 'game_fruit') {
-
+    let leftA = result[6][1];//右肩Y
+    let rightA = result[5][1];//左肩Y
+    let leftB = result[10][1];//右手Y
+    let rightB = result[9][1];//左手Y
+    let bizi = result[0][1];//鼻子Y
+    if (leftB < leftA || rightB < rightA || leftB < bizi || rightB < bizi) {
+      let myIndex = areaStateList.value.findIndex((item: any) => {
+        return item.area == area;
+      })
+      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] }];
+      areaStateList.value[myIndex].state = true;
+      areaStateList.value[myIndex].boxes = boxes;
+    }
   }
 };
 
+/**
+ * 计算B点的夹角度数(∠ABC)
+ * @param {Object} pointA - A点坐标 {x, y, z可选}
+ * @param {Object} pointB - B点坐标 {x, y, z可选}
+ * @param {Object} pointC - C点坐标 {x, y, z可选}
+ * @returns {number} B点的夹角度数(保留两位小数)
+ */
+const calculateAngleAtB = (pointA, pointB, pointC) => {
+  // 计算向量BA和向量BC
+  const vectorBA = {
+    x: pointA.x - pointB.x,
+    y: pointA.y - pointB.y,
+    z: (pointA.z || 0) - (pointB.z || 0)
+  };
+
+  const vectorBC = {
+    x: pointC.x - pointB.x,
+    y: pointC.y - pointB.y,
+    z: (pointC.z || 0) - (pointB.z || 0)
+  };
+
+  // 计算点积
+  const dotProduct =
+    vectorBA.x * vectorBC.x +
+    vectorBA.y * vectorBC.y +
+    vectorBA.z * vectorBC.z;
+
+  // 计算向量BA的模长
+  const lengthBA = Math.sqrt(
+    vectorBA.x ** 2 +
+    vectorBA.y ** 2 +
+    vectorBA.z ** 2
+  );
+
+  // 计算向量BC的模长
+  const lengthBC = Math.sqrt(
+    vectorBC.x ** 2 +
+    vectorBC.y ** 2 +
+    vectorBC.z ** 2
+  );
+
+  // 防止除以零的情况
+  if (lengthBA === 0 || lengthBC === 0) {
+    throw new Error("点A、B、C不能重合");
+  }
+
+  // 计算余弦值
+  const cosine = dotProduct / (lengthBA * lengthBC);
+
+  // 由于计算误差可能导致cosine略超出[-1, 1]范围,需要修正
+  const clampedCosine = Math.max(Math.min(cosine, 1), -1);
+
+  // 计算弧度并转换为角度
+  const angleRadians = Math.acos(clampedCosine);
+  const angleDegrees = angleRadians * (180 / Math.PI);
+
+  // 保留两位小数并返回
+  return parseFloat(angleDegrees.toFixed(2));
+}
 
 /**
  * 打开
  */
 const getOpen = async (game: any, area: any) => {
+  lock.value = false;
   currentGame.value = game;
   currentGameArea.value = area;
   let list = area.map((item: any) => {
@@ -107,10 +253,42 @@ const getOpen = async (game: any, area: any) => {
  * 关闭
 */
 const getExit = () => {
+  speckCancel(); //停止播报
   actionState.value = false;
   emit('confirmExit', {});
 };
 
+/**
+ * 监听全部OK
+ */
+watch(
+  () => areaStateList.value,
+  (newData, oldData) => {
+    //都准备就绪就开始
+    if (lock.value) {
+      return false;
+    }
+    let state = newData.every((item: any) => {
+      return item.state;
+    })
+    if (state) {
+      lock.value = true;
+      let num = 3;
+      let timer: any = setInterval(() => {
+        if (num == 1) {
+          actionState.value = false;
+          clearInterval(timer);
+          timer = null;
+          emit('confirmStart', areaStateList.value);
+        }
+        speckText(num);
+        num--;
+      }, 1000)
+    }
+  },
+  { deep: true }
+);
+
 //暴露给父组件用
 defineExpose({
   getOpen,
@@ -134,7 +312,7 @@ defineExpose({
   left: 50%;
   top: 50%;
   margin-left: calc(70rem / -2);
-  margin-top: calc(((70rem / 2) + 3.2rem) / -2);
+  margin-top: calc(((70rem / 2) + 12rem) / -2);
   display: flex;
   flex-direction: column;
   z-index: 999;
@@ -146,18 +324,130 @@ defineExpose({
     display: flex;
     align-items: center;
     justify-content: center;
+    flex-direction: column;
     margin-bottom: 20px;
-    background: linear-gradient(62deg, #092941 -85%, #2A484B 96%);
+    background: linear-gradient(59deg, #092941 -85%, #2A484B 96%);
+    box-shadow: inset 0px 1px 0px 2px rgba(255, 255, 255, 0.5577);
+
+    .prompt {
+      color: #ffffff;
+      font-size: 2.5vh;
+      display: flex;
+      line-height: 3vh;
+      padding-top: 2vh;
+
+      i {
+        width: 3vh;
+        height: 3vh;
+        display: block;
+        background-image: url('@/assets/images/game/prompt.png');
+        background-position: bottom;
+        background-repeat: no-repeat;
+        background-size: 100%;
+      }
+    }
 
     .confirmDiaWindow-con {
+      width: 85%;
       padding: 25px;
+      display: flex;
+      justify-content: center;
+      flex-direction: column;
 
-      .pic {
+      .picBox {
 
         width: 100%;
+        background-image: url('@/assets/images/game/bg.png');
+        background-position: bottom;
+        background-repeat: no-repeat;
+        background-size: 100%;
+        display: flex;
+        justify-content: center;
+        padding-bottom: 10vh;
+
+        .pic {
+          display: flex;
+          justify-content: center;
+          flex-direction: column;
+
+          .gamePic {
+            border: 5px solid #3BDDCE;
+            width: 60vh;
+            height: calc(60vh * (1080 / 1920));
+
+            img {
+              width: 100%;
+              height: 100%;
+            }
+          }
+
+          .gamePlayer {
+            display: flex;
+            width: 60vh;
+            margin-top: -20vh;
+
+            .item {
+              flex: 1;
+              display: flex;
+              justify-content: center;
+
+              .player {
+                position: relative;
+                width: 22vh;
+                height: calc(22vh * (1000 / 518));
+              }
+
+              .fade-image {
+                width: 100%;
+                height: 100%;
+                position: absolute;
+                left: 0;
+                top: 0;
+              }
+
+              .fade-image1 {
+                animation: fadeOpacity1 2s infinite;
+              }
+
+              .fade-image2 {
+                animation: fadeOpacity2 2s infinite;
+              }
+
+              .ok {
+                width: 8vh;
+                height: 8vh;
+                position: absolute;
+                top: 50%;
+                left: 50%;
+                margin-top: 1vh;
+                margin-left: -4vh;
+
+                img {
+                  width: 100%;
+                }
+              }
+
+              @keyframes fadeOpacity1 {
+                0% {
+                  opacity: 1;
+                }
+
+                100% {
+                  opacity: 0;
+                }
+              }
+
+              @keyframes fadeOpacity2 {
+                0% {
+                  opacity: 0;
+                }
 
-        img {
-          width: 100%;
+                100% {
+                  opacity: 1;
+                }
+              }
+            }
+          }
         }
       }
 
@@ -165,7 +455,7 @@ defineExpose({
         width: 100%;
         color: #ffffff;
         font-size: 2.8vh;
-        margin-bottom: 30px;
+        padding-top: 20px;
       }
     }
   }

+ 16 - 6
src/views/game/index.vue

@@ -21,7 +21,7 @@
     </div>
     <Transition :enter-active-class="proxy?.animate.rankingWindow.enter"
       :leave-active-class="proxy?.animate.rankingWindow.leave">
-      <div class="gameWindow" v-if="currentGame">
+      <div class="gameWindow" v-if="start">
         <!---人体姿态识别-->
         <div class="columns" v-if="currentGame == 'bodyposecontroller'">
           <template v-if="currentGameArea.length == 2">
@@ -61,7 +61,8 @@
       </div>
     </Transition>
     <div class="close" v-if="currentGame" @click="getExitGame"></div>
-    <ActionConfirmWindow ref="actionConfirmRef" @confirmExit="getExitGame(1)"></ActionConfirmWindow>
+    <ActionConfirmWindow ref="actionConfirmRef" @confirmExit="getExitGame(1)" @confirmStart="getStartGame">
+    </ActionConfirmWindow>
   </div>
 </template>
 
@@ -97,8 +98,10 @@ const data = reactive<any>({
   deviceInfo: {},//设备信息
   currentGame: "",//当前游戏
   currentGameArea: [],//当前游戏区
+  start: false,//是否开始游戏
+  areaStateList: [],//各游戏就绪状态
 });
-const { projectList, wsState, bodyposeState, deviceInfo, currentGame, currentGameArea } = toRefs(data);
+const { projectList, wsState, bodyposeState, deviceInfo, currentGame, currentGameArea, start, areaStateList } = toRefs(data);
 
 /**
  * 初始化
@@ -220,7 +223,7 @@ const getJump = (data: any) => {
   } else {
     currentGame.value = data.exam_name;
     currentGameArea.value = data.area_test_id?.split(",") || [];
-    actionConfirmRef.value.getOpen(currentGame.value,currentGameArea.value);
+    actionConfirmRef.value.getOpen(currentGame.value, currentGameArea.value);
     currentGameArea.value.forEach((item: any) => {
       checkBodypose(item);
     })
@@ -238,7 +241,8 @@ const getExit = () => {
 /**
  * 退出游戏
  */
-const getExitGame = (type:any) => {
+const getExitGame = (type: any) => {
+  start.value = false;
   if (type == 1) {
     currentGameArea.value.forEach((item: any) => {
       terminateBodypose(item);
@@ -253,8 +257,14 @@ const getExitGame = (type:any) => {
     }).finally(() => {
     });
   }
+};
 
-
+/**
+ * 开始游戏
+ */
+const getStartGame = (data: any) => {
+  start.value = true;
+  areaStateList.value = data;
 };