Parcourir la source

feat:the switch of the game page

0225273 il y a 1 semaine
Parent
commit
449a5777a2

+ 93 - 1
MindOCApp/package-lock.json

@@ -22,7 +22,11 @@
         "@capacitor/keyboard": "7.0.1",
         "@capacitor/status-bar": "7.0.1",
         "@ionic/angular": "^8.0.0",
+        "@types/howler": "^2.2.12",
+        "@types/pixi.js": "^5.0.0",
+        "howler": "^2.2.4",
         "ionicons": "^7.0.0",
+        "pixi.js": "^8.9.1",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
         "zone.js": "~0.15.0"
@@ -5206,6 +5210,11 @@
       "dev": true,
       "optional": true
     },
+    "node_modules/@pixi/colord": {
+      "version": "2.9.6",
+      "resolved": "https://registry.npmmirror.com/@pixi/colord/-/colord-2.9.6.tgz",
+      "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="
+    },
     "node_modules/@pkgjs/parseargs": {
       "version": "0.11.0",
       "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -5684,6 +5693,16 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/css-font-loading-module": {
+      "version": "0.0.12",
+      "resolved": "https://registry.npmmirror.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
+      "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA=="
+    },
+    "node_modules/@types/earcut": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmmirror.com/@types/earcut/-/earcut-2.1.4.tgz",
+      "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ=="
+    },
     "node_modules/@types/eslint": {
       "version": "9.6.1",
       "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz",
@@ -5755,6 +5774,11 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/howler": {
+      "version": "2.2.12",
+      "resolved": "https://registry.npmmirror.com/@types/howler/-/howler-2.2.12.tgz",
+      "integrity": "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g=="
+    },
     "node_modules/@types/http-errors": {
       "version": "2.0.4",
       "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.4.tgz",
@@ -5812,6 +5836,15 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/pixi.js": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/@types/pixi.js/-/pixi.js-5.0.0.tgz",
+      "integrity": "sha512-yZqQBR043lRBlBZci2cx6hgmX0fvBfYIqFm6VThlnueXEjitxd3coy+BGsqsZ7+ary7O//+ks4aJRhC5MJoHqA==",
+      "deprecated": "This is a stub types definition. pixi.js provides its own type definitions, so you do not need this installed.",
+      "dependencies": {
+        "pixi.js": "*"
+      }
+    },
     "node_modules/@types/qs": {
       "version": "6.9.18",
       "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.18.tgz",
@@ -6235,11 +6268,15 @@
         "@xtuc/long": "4.2.2"
       }
     },
+    "node_modules/@webgpu/types": {
+      "version": "0.1.60",
+      "resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.60.tgz",
+      "integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA=="
+    },
     "node_modules/@xmldom/xmldom": {
       "version": "0.8.10",
       "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
       "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
-      "dev": true,
       "engines": {
         "node": ">=10.0.0"
       }
@@ -8161,6 +8198,11 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/earcut": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmmirror.com/earcut/-/earcut-2.2.4.tgz",
+      "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="
+    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -9621,6 +9663,14 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/gifuct-js": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/gifuct-js/-/gifuct-js-2.1.2.tgz",
+      "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
+      "dependencies": {
+        "js-binary-schema-parser": "^2.0.3"
+      }
+    },
     "node_modules/glob": {
       "version": "7.2.3",
       "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
@@ -9862,6 +9912,11 @@
       "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
       "dev": true
     },
+    "node_modules/howler": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmmirror.com/howler/-/howler-2.2.4.tgz",
+      "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
+    },
     "node_modules/hpack.js": {
       "version": "2.1.6",
       "resolved": "https://registry.npmmirror.com/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -10751,6 +10806,11 @@
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "dev": true
     },
+    "node_modules/ismobilejs": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/ismobilejs/-/ismobilejs-1.1.1.tgz",
+      "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="
+    },
     "node_modules/isobject": {
       "version": "3.0.1",
       "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz",
@@ -10903,6 +10963,11 @@
         "jiti": "bin/jiti.js"
       }
     },
+    "node_modules/js-binary-schema-parser": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
+      "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="
+    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -13130,6 +13195,11 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/parse-svg-path": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmmirror.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
+      "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="
+    },
     "node_modules/parse5": {
       "version": "7.2.1",
       "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.2.1.tgz",
@@ -13293,6 +13363,28 @@
         "@napi-rs/nice": "^1.0.1"
       }
     },
+    "node_modules/pixi.js": {
+      "version": "8.9.1",
+      "resolved": "https://registry.npmmirror.com/pixi.js/-/pixi.js-8.9.1.tgz",
+      "integrity": "sha512-2vF5Yu9WC/83ly2tCGkjb+ZGnrr+vlKtZezmD0AmJEQoYZO5nL94806l+PVcJBKW6qrF0YHtbh0ubb6CB7/8Rg==",
+      "dependencies": {
+        "@pixi/colord": "^2.9.6",
+        "@types/css-font-loading-module": "^0.0.12",
+        "@types/earcut": "^2.1.4",
+        "@webgpu/types": "^0.1.40",
+        "@xmldom/xmldom": "^0.8.10",
+        "earcut": "^2.2.4",
+        "eventemitter3": "^5.0.1",
+        "gifuct-js": "^2.1.2",
+        "ismobilejs": "^1.1.1",
+        "parse-svg-path": "^0.1.2"
+      }
+    },
+    "node_modules/pixi.js/node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
+    },
     "node_modules/pkg-dir": {
       "version": "7.0.0",
       "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-7.0.0.tgz",

+ 4 - 0
MindOCApp/package.json

@@ -27,7 +27,11 @@
     "@capacitor/keyboard": "7.0.1",
     "@capacitor/status-bar": "7.0.1",
     "@ionic/angular": "^8.0.0",
+    "@types/howler": "^2.2.12",
+    "@types/pixi.js": "^5.0.0",
+    "howler": "^2.2.4",
     "ionicons": "^7.0.0",
+    "pixi.js": "^8.9.1",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.15.0"

+ 30 - 15
MindOCApp/src/app/game/game.page.html

@@ -1,17 +1,32 @@
-<ion-header [translucent]="true">
-  <ion-toolbar>
-    <ion-title>
-      Tab 2
-    </ion-title>
-  </ion-toolbar>
-</ion-header>
+<ion-content  class="game-container">
+  <!-- 游戏模式切换 -->
+  <div class="game-mode-switch">
+    <ion-segment [(ngModel)]="currentGame" (ionChange)="switchGameMode()">
+      <ion-segment-button value="bubble">
+        <ion-label style="font-size: 16px;">捏泡泡</ion-label>
+      </ion-segment-button>
+      <ion-segment-button value="sand">
+        <ion-label style="font-size: 16px;">禅意沙画</ion-label>
+      </ion-segment-button>
+    </ion-segment>
+  </div>
 
-<ion-content [fullscreen]="true">
-  <ion-header collapse="condense">
-    <ion-toolbar>
-      <ion-title size="large">Tab 2</ion-title>
-    </ion-toolbar>
-  </ion-header>
-
-  <app-explore-container name="Tab 2 page"></app-explore-container>
+  <!-- 游戏画布容器 -->
+  <div #gameCanvasContainer class="canvas-container">
+    <canvas #gameCanvas></canvas>
+  </div>
+  
+  <!-- 游戏控制面板 -->
+  <div class="game-panel">
+    <div class="score-board">
+      <ion-icon name="trophy"></ion-icon>
+      <span>已解压:{{ score }}次</span>
+    </div>
+    <ion-button fill="clear" (click)="toggleSound()">
+      <ion-icon 
+        [name]="isMuted ? 'volume-mute' : 'volume-high'"
+        [color]="isMuted ? 'medium' : 'primary'"
+      ></ion-icon>
+    </ion-button>
+  </div>  
 </ion-content>

+ 102 - 0
MindOCApp/src/app/game/game.page.scss

@@ -0,0 +1,102 @@
+/* game.page.scss */
+.game-container {
+    --game-primary: #FF9A9E;
+    --game-secondary: #A8EDEA;
+    --game-accent: #6C5B7B;
+  
+    height: 100vh;
+    overflow: hidden;
+  
+    .game-mode-switch {
+      position: absolute;
+      top: 16px;
+      left: 50%;
+      transform: translateX(-50%);
+      z-index: 10;
+      width: 80%;
+      max-width: 400px;
+  
+      ion-segment {
+        --background: #ff9a9e;
+        backdrop-filter: blur(10px);
+        height: 40px;
+      }
+      
+    }
+  
+    .canvas-container {
+      width: 100%;
+      height: calc(100% - 60px);
+      touch-action: none;
+    }
+  
+    .game-panel {
+      position: absolute;
+      bottom: 20px;
+      left: 20px;
+      right: 20px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px 20px;
+      background: rgba(255,255,255,0.9);
+      border-radius: 30px;
+      backdrop-filter: blur(8px);
+  
+      .score-board {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        color: var(--game-accent);
+        font-weight: 500;
+  
+        ion-icon {
+          color: #FFD700;
+          font-size: 24px;
+        }
+      }
+    }
+  
+    .achievement-toast {
+      position: fixed;
+      top: -60px;
+      left: 50%;
+      transform: translateX(-50%);
+      background: rgba(0,0,0,0.8);
+      color: white;
+      padding: 12px 24px;
+      border-radius: 30px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      transition: top 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
+  
+      &.show {
+        top: 20px;
+      }
+  
+      ion-icon {
+        color: #FFD700;
+      }
+    }
+  }
+  
+  /* 泡泡爆破动画 */
+  @keyframes pop {
+    0% { transform: scale(1); opacity: 1; }
+    100% { transform: scale(1.5); opacity: 0; }
+  }
+
+ion-icon[name^="volume"] {
+transition: transform 0.3s ease;
+
+&[name="volume-mute"] {
+    animation: shake 0.5s ease;
+}
+}
+
+@keyframes shake {
+0%, 100% { transform: translateX(0); }
+25% { transform: translateX(-3px); }
+75% { transform: translateX(3px); }
+}

+ 164 - 2
MindOCApp/src/app/game/game.page.ts

@@ -1,4 +1,8 @@
-import { Component } from '@angular/core';
+// game.page.ts
+import { Component, ViewChild, ElementRef, AfterViewInit, HostListener } from '@angular/core';
+import * as PIXI from 'pixi.js';
+import { Howl, Howler } from 'howler';
+import { Haptics, ImpactStyle } from '@capacitor/haptics';
 
 @Component({
   selector: 'app-game',
@@ -6,7 +10,165 @@ import { Component } from '@angular/core';
   styleUrls: ['game.page.scss'],
   standalone: false,
 })
-export class GamePage {
+export class GamePage implements AfterViewInit {
+
+  @ViewChild('gameCanvas') canvasRef!: ElementRef;
+  @ViewChild('gameCanvasContainer') containerRef!: ElementRef;
+  
+  // 游戏状态
+  currentGame: 'bubble' | 'sand' = 'bubble';
+  score = 0;
+  isMuted = false;
+  showAchievement = false;
+  achievementText = '';
+  achievementIcon = '';
+
+  // PIXI实例
+  private app!: PIXI.Application;
+  private bubbles: PIXI.Sprite[] = [];
+  private sandTexture!: PIXI.RenderTexture;
+
+  ngAfterViewInit() {
+    this.initPixi();
+    this.loadAssets();
+  }
+
+  private initPixi(){
+
+  }
+
+  private loadAssets(){
+    PIXI.Assets.load([
+      { alias: 'bubble', src: 'assets/images/bubble.png' },
+      { alias: 'brush', src: 'assets/images/brush.png' }
+    ]).then(() => {
+      //this.initBubbleGame();
+      //this.initSandGame();
+      this.switchGameMode();
+    });
+  }
+
+  /** 切换游戏模式 */
+  switchGameMode() {
+    this.app.stage.removeChildren();
+    if (this.currentGame === 'bubble') {
+      this.app.stage.addChild(...this.bubbles);
+    } else {
+      this.initSandDrawing();
+    }
+  }
+
+  /** 沙画绘制逻辑 */
+  private initSandDrawing() {
+    const brush = PIXI.Sprite.from('brush');
+    brush.anchor.set(0.5);
+    brush.visible = false;
+
+    const sandGraphics = new PIXI.Graphics();
+    let drawing = false;
+    let lastPoint: PIXI.Point | null = null;
+
+    // 触摸/鼠标事件
+    this.app.stage.eventMode = 'static';
+    this.app.stage.hitArea = this.app.screen;
+    this.app.stage
+      .on('pointerdown', (e: PIXI.FederatedPointerEvent) => {
+        drawing = true;
+        lastPoint = e.global.clone();
+        brush.position.copyFrom(e.global);
+        brush.visible = true;
+      })
+      .on('pointermove', (e: PIXI.FederatedPointerEvent) => {
+        if (!drawing) return;
+        
+        // 绘制沙粒轨迹
+        //sandGraphics.lineStyle({ width: 8, color: 0xf4d03f, alpha: 0.7 });
+        if (lastPoint) {
+          sandGraphics.moveTo(lastPoint.x, lastPoint.y);
+          sandGraphics.lineTo(e.global.x, e.global.y);
+        }
+        lastPoint = e.global.clone();
+
+        // 更新笔刷位置
+        brush.position.copyFrom(e.global);
+        this.playSound('draw');
+      })
+      .on('pointerup', () => {
+        drawing = false;
+        brush.visible = false;
+        lastPoint = null;
+      });
+
+    this.app.stage.addChild(sandGraphics, brush);
+  }
+
+  // 音效
+  private sounds = {
+    pop: new Howl({ src: ['assets/sfx/pop.mp3'] }),
+    draw: new Howl({ src: ['assets/sfx/sand-draw.mp3'] }),
+    unlock: new Howl({ src: ['assets/sfx/achievement.mp3'] })
+  };
+
+  /** 播放音效 */
+  private playSound(type: keyof typeof this.sounds) {
+    if (!this.isMuted) {
+      this.sounds[type].play();
+    }
+  }
+
+  /** 
+   * 切换声音状态 
+   * 功能说明:
+   * 1. 切换全局静音状态
+   * 2. 控制所有音效实例
+   * 3. 提供触觉反馈
+   * 4. 保存状态到本地存储
+   */
+   async toggleSound() {
+    try {
+      // 切换静音状态
+      this.isMuted = !this.isMuted;
+
+      // 触觉反馈(需要Capacitor支持)
+      await Haptics.impact({ style: ImpactStyle.Light });
+
+      // 控制全局音频
+      Howler.mute(this.isMuted);
+
+      // 同步控制单独音效实例(可选)
+      Object.values(this.sounds).forEach(sound => {
+        sound.mute(this.isMuted);
+      });
+
+      // 保存状态到本地存储
+      localStorage.setItem('gameMuteState', JSON.stringify(this.isMuted));
+
+      // 显示状态提示
+      this.showSoundStateToast();
+    } catch (error) {
+      console.error('切换声音时出错:', error);
+    }
+  }
+
+  /** 显示声音状态提示 */
+  private showSoundStateToast() {
+    this.achievementText = this.isMuted ? '静音模式已开启' : '声音模式已开启';
+    this.achievementIcon = this.isMuted ? 'volume-mute' : 'volume-high';
+    this.showAchievement = true;
+    
+    setTimeout(() => {
+      this.showAchievement = false;
+    }, 1500);
+  }
+
+  /** 初始化时读取静音状态 */
+  private initSoundState() {
+    const savedState = localStorage.getItem('gameMuteState');
+    if (savedState) {
+      this.isMuted = JSON.parse(savedState);
+      Howler.mute(this.isMuted);
+    }
+  }
 
   constructor() {}
 

+ 18 - 0
MindOCApp/src/app/mood/mood.page.scss

@@ -361,6 +361,24 @@
       &[class*="recording"] {
         animation: pulse 1.5s infinite;
       }
+      // 录音状态指示
+      .recording-indicator {
+        position: fixed;
+        top: 20px;
+        right: 20px;
+        width: 12px;
+        height: 12px;
+        background: #ff4757;
+        border-radius: 50%;
+        animation: pulse 1.5s infinite;
+        
+        @keyframes pulse {
+          0% { transform: scale(0.8); opacity: 0.5; }
+          50% { transform: scale(1.2); opacity: 1; }
+          100% { transform: scale(0.8); opacity: 0.5; }
+        }
+      }
+
     }
   
 

BIN
MindOCApp/src/assets/images/brush.webp


BIN
MindOCApp/src/assets/images/bubble.webp


BIN
MindOCApp/src/assets/sfx/achievement.mp3


BIN
MindOCApp/src/assets/sfx/pop.mp3


BIN
MindOCApp/src/assets/sfx/sand-draw.mp3


+ 1 - 1
MindOCApp/tsconfig.json

@@ -19,7 +19,7 @@
     "target": "es2022",
     "module": "es2020",
     "lib": [
-      "es2018", 
+      "es2020", 
       "dom"
     ],
     "useDefineForClassFields": false