|
@@ -0,0 +1,335 @@
|
|
|
+import { Component, OnInit, ViewChild, ElementRef, Renderer2 } from '@angular/core';
|
|
|
+import { Router } from '@angular/router';
|
|
|
+import { IonModal } from '@ionic/angular';
|
|
|
+import { Storage } from '@ionic/storage-angular';
|
|
|
+
|
|
|
+interface Bubble {
|
|
|
+ x: number;
|
|
|
+ y: number;
|
|
|
+ text: string;
|
|
|
+ color: string;
|
|
|
+ scale: number;
|
|
|
+ burst: boolean;
|
|
|
+ speedX: number;
|
|
|
+ speedY: number;
|
|
|
+ type: 'normal' | 'hard' | 'fragile'; // 气泡类型
|
|
|
+ health: number; // 生命值
|
|
|
+ hits: number; // 已击中次数
|
|
|
+}
|
|
|
+
|
|
|
+@Component({
|
|
|
+ selector: 'app-emotion-vent',
|
|
|
+ templateUrl: './emotion-vent.page.html',
|
|
|
+ styleUrls: ['./emotion-vent.page.scss'],
|
|
|
+ standalone:false,
|
|
|
+})
|
|
|
+export class EmotionVentPage implements OnInit {
|
|
|
+ @ViewChild('particlesContainer', { static: false }) particlesContainer!: ElementRef;
|
|
|
+ @ViewChild('keywordModal') keywordModal!: IonModal;
|
|
|
+ newKeyword = '';
|
|
|
+ customKeywords: string[] = [];
|
|
|
+ bubbles: Bubble[] = [];
|
|
|
+ burstCount = 0;
|
|
|
+ hammerX = 0;
|
|
|
+ hammerY = 0;
|
|
|
+ hammerVisible = false;
|
|
|
+ negativeWords = [
|
|
|
+ '加班', '分手', '压力', '焦虑', '失眠', '孤独', '失败',
|
|
|
+ '被拒', '拖延', '迷茫', '自卑', '愤怒', '委屈', '失望',
|
|
|
+ '疲惫', '无助', '内卷', 'PUA', '崩溃', 'emo'
|
|
|
+ ];
|
|
|
+ colors = [
|
|
|
+ '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
|
|
|
+ '#F06292', '#7986CB', '#9575CD', '#64B5F6', '#4DB6AC'
|
|
|
+ ];
|
|
|
+ animationFrameId: number=0;
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private router: Router,
|
|
|
+ private renderer: Renderer2,
|
|
|
+ private storage: Storage
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ async ngOnInit() {
|
|
|
+ await this.storage.create();
|
|
|
+ this.loadCustomKeywords();
|
|
|
+ this.generateBubbles();
|
|
|
+ this.startBubbleMovement();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开关键词模态框
|
|
|
+async openKeywordModal() {
|
|
|
+ await this.loadCustomKeywords();
|
|
|
+ this.keywordModal.present();
|
|
|
+}
|
|
|
+
|
|
|
+// 关闭模态框
|
|
|
+dismissModal() {
|
|
|
+ this.keywordModal.dismiss();
|
|
|
+}
|
|
|
+
|
|
|
+// 添加关键词
|
|
|
+async addKeyword() {
|
|
|
+ if (this.newKeyword.trim() && !this.customKeywords.includes(this.newKeyword.trim())) {
|
|
|
+ this.customKeywords.push(this.newKeyword.trim());
|
|
|
+ await this.storage.set('customKeywords', this.customKeywords);
|
|
|
+ this.newKeyword = '';
|
|
|
+ this.resetBubbles(); // 重新生成气泡以包含新关键词
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 移除关键词
|
|
|
+async removeKeyword(keyword: string) {
|
|
|
+ this.customKeywords = this.customKeywords.filter(k => k !== keyword);
|
|
|
+ await this.storage.set('customKeywords', this.customKeywords);
|
|
|
+ this.resetBubbles(); // 重新生成气泡
|
|
|
+}
|
|
|
+
|
|
|
+// 加载保存的关键词
|
|
|
+async loadCustomKeywords() {
|
|
|
+ const savedKeywords = await this.storage.get('customKeywords');
|
|
|
+ this.customKeywords = savedKeywords || [];
|
|
|
+}
|
|
|
+
|
|
|
+ // 修改generateBubbles方法
|
|
|
+generateBubbles() {
|
|
|
+ const bubbleCount = 15;
|
|
|
+ this.bubbles = [];
|
|
|
+ this.burstCount = 0;
|
|
|
+ // 合并默认关键词和自定义关键词
|
|
|
+ const allKeywords = [...this.negativeWords, ...this.customKeywords];
|
|
|
+
|
|
|
+ for (let i = 0; i < bubbleCount; i++) {
|
|
|
+ const typeRoll = Math.random();
|
|
|
+ const randomWord = allKeywords[Math.floor(Math.random() * allKeywords.length)];
|
|
|
+ let type: 'normal' | 'hard' | 'fragile', health: number;
|
|
|
+
|
|
|
+ if (typeRoll < 0.2) { // 20%几率生成坚硬气泡
|
|
|
+ type = 'hard';
|
|
|
+ health = 2;
|
|
|
+ } else if (typeRoll < 0.4) { // 20%几率生成脆弱气泡
|
|
|
+ type = 'fragile';
|
|
|
+ health = 1;
|
|
|
+ } else { // 60%普通气泡
|
|
|
+ type = 'normal';
|
|
|
+ health = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ //const randomWord = this.negativeWords[Math.floor(Math.random() * this.negativeWords.length)];
|
|
|
+ const randomColor = this.getColorForType(type);
|
|
|
+
|
|
|
+ this.bubbles.push({
|
|
|
+ x: Math.random() * (window.innerWidth - 100),
|
|
|
+ y: Math.random() * (window.innerHeight - 100),
|
|
|
+ text: randomWord,
|
|
|
+ color: randomColor,
|
|
|
+ scale: this.getScaleForType(type),
|
|
|
+ burst: false,
|
|
|
+ speedX: (Math.random() - 0.5) * 2,
|
|
|
+ speedY: (Math.random() - 0.5) * 2,
|
|
|
+ type: type,
|
|
|
+ health: health,
|
|
|
+ hits: 0
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 根据类型获取颜色
|
|
|
+getColorForType(type: string): string {
|
|
|
+ switch(type) {
|
|
|
+ case 'hard':
|
|
|
+ return '#FF6B6B'; // 红色系
|
|
|
+ case 'fragile':
|
|
|
+ return '#4ECDC4'; // 青色系
|
|
|
+ default:
|
|
|
+ return this.colors[Math.floor(Math.random() * this.colors.length)];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 根据类型获取大小
|
|
|
+getScaleForType(type: string): number {
|
|
|
+ switch(type) {
|
|
|
+ case 'hard':
|
|
|
+ return 0.9 + Math.random() * 0.3; // 稍大
|
|
|
+ case 'fragile':
|
|
|
+ return 0.7 + Math.random() * 0.2; // 稍小
|
|
|
+ default:
|
|
|
+ return 0.8 + Math.random() * 0.4;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+ startBubbleMovement() {
|
|
|
+ const moveBubbles = () => {
|
|
|
+ this.bubbles.forEach(bubble => {
|
|
|
+ if (!bubble.burst) {
|
|
|
+ bubble.x += bubble.speedX;
|
|
|
+ bubble.y += bubble.speedY;
|
|
|
+
|
|
|
+ // 边界检测
|
|
|
+ if (bubble.x <= 0 || bubble.x >= window.innerWidth - 80) {
|
|
|
+ bubble.speedX *= -1;
|
|
|
+ }
|
|
|
+ if (bubble.y <= 0 || bubble.y >= window.innerHeight - 80) {
|
|
|
+ bubble.speedY *= -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.animationFrameId = requestAnimationFrame(moveBubbles);
|
|
|
+ };
|
|
|
+
|
|
|
+ moveBubbles();
|
|
|
+ }
|
|
|
+
|
|
|
+ onTouchMove(event: TouchEvent) {
|
|
|
+ if (event.touches.length > 0) {
|
|
|
+ this.hammerX = event.touches[0].clientX;
|
|
|
+ this.hammerY = event.touches[0].clientY;
|
|
|
+
|
|
|
+ // 检测碰撞
|
|
|
+ this.checkCollision(this.hammerX, this.hammerY);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ onTouchEnd() {
|
|
|
+ // 触摸结束时可以添加一些效果
|
|
|
+ }
|
|
|
+
|
|
|
+ checkCollision(x: number, y: number) {
|
|
|
+ this.bubbles.forEach(bubble => {
|
|
|
+ if (!bubble.burst) {
|
|
|
+ const distance = Math.sqrt(
|
|
|
+ Math.pow(x - (bubble.x + 40), 2) +
|
|
|
+ Math.pow(y - (bubble.y + 40), 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (distance < 40) {
|
|
|
+ this.burstBubble(bubble);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ burstBubble(bubble: Bubble) {
|
|
|
+ // 增加击中次数
|
|
|
+ bubble.hits++;
|
|
|
+
|
|
|
+ // 如果未达到爆裂条件,只显示击中效果
|
|
|
+ if (bubble.hits < bubble.health) {
|
|
|
+ this.showHitEffect(bubble);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!bubble.burst) {
|
|
|
+ bubble.burst = true;
|
|
|
+ this.burstCount++;
|
|
|
+
|
|
|
+ // 根据不同类型播放不同音效
|
|
|
+ this.playBurstSound(bubble.type);
|
|
|
+
|
|
|
+
|
|
|
+ // 所有气泡都爆裂后显示完成消息
|
|
|
+ if (this.burstCount === this.bubbles.length) {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.showCompletionMessage();
|
|
|
+ }, 1000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 显示击中效果
|
|
|
+showHitEffect(bubble: Bubble) {
|
|
|
+ // 震动效果
|
|
|
+ const element = document.querySelector(`.negative-bubble[style*="left: ${bubble.x}px"][style*="top: ${bubble.y}px"]`);
|
|
|
+ if (element) {
|
|
|
+ element.classList.add('shake');
|
|
|
+ setTimeout(() => {
|
|
|
+ element.classList.remove('shake');
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示裂纹效果
|
|
|
+ this.showCrackEffect(bubble);
|
|
|
+
|
|
|
+ // 播放击中音效
|
|
|
+ this.playHitSound();
|
|
|
+}
|
|
|
+
|
|
|
+// 显示裂纹效果
|
|
|
+showCrackEffect(bubble: Bubble) {
|
|
|
+ const crack = document.createElement('div');
|
|
|
+ crack.className = 'crack-effect';
|
|
|
+ crack.innerHTML = '💢'; // 可以使用Unicode字符或SVG
|
|
|
+
|
|
|
+ const element = document.querySelector(`.negative-bubble[style*="left: ${bubble.x}px"][style*="top: ${bubble.y}px"]`);
|
|
|
+ if (element) {
|
|
|
+ crack.style.position = 'absolute';
|
|
|
+ crack.style.fontSize = '30px';
|
|
|
+ crack.style.animation = 'fadeInOut 1s';
|
|
|
+ element.appendChild(crack);
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ crack.remove();
|
|
|
+ }, 1000);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+ playBurstSound(type: string) {
|
|
|
+ // 这里可以添加爆裂音效
|
|
|
+ const sounds = [
|
|
|
+ 'assets/sounds/pop1.mp3',
|
|
|
+ ];
|
|
|
+ const sound = new Audio(sounds[Math.floor(Math.random() * sounds.length)]);
|
|
|
+ sound.volume = 0.3;
|
|
|
+ sound.play().catch(e => console.log('Audio play error:', e));
|
|
|
+ }
|
|
|
+
|
|
|
+ resetBubbles() {
|
|
|
+ cancelAnimationFrame(this.animationFrameId);
|
|
|
+ this.generateBubbles();
|
|
|
+ this.startBubbleMovement();
|
|
|
+ }
|
|
|
+
|
|
|
+// 添加点击处理方法
|
|
|
+onBubbleClick(bubble: Bubble, event: MouseEvent) {
|
|
|
+ //this.createRipple(event, bubble);
|
|
|
+ this.burstBubble(bubble);
|
|
|
+}
|
|
|
+
|
|
|
+// // 播放击中音效
|
|
|
+playHitSound() {
|
|
|
+ const sounds = [
|
|
|
+ 'assets/sounds/pop2.mp3'
|
|
|
+ ];
|
|
|
+ const sound = new Audio(sounds[Math.floor(Math.random() * sounds.length)]);
|
|
|
+ sound.volume = 0.2;
|
|
|
+ sound.play().catch(e => console.log('Audio play error:', e));
|
|
|
+}
|
|
|
+
|
|
|
+showCompletionMessage() {
|
|
|
+ // 创建更漂亮的完成消息
|
|
|
+ const message = document.createElement('div');
|
|
|
+ message.innerHTML = `
|
|
|
+ <div style="
|
|
|
+ position: fixed;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ background: rgba(0,0,0,0.8);
|
|
|
+ color: white;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 10px;
|
|
|
+ text-align: center;
|
|
|
+ z-index: 100;
|
|
|
+ animation: zoomIn 0.5s;
|
|
|
+ ">
|
|
|
+ <h2>🎉 干得漂亮! 🎉</h2>
|
|
|
+ <p>你已经粉碎了所有负面情绪!</p>
|
|
|
+ <ion-button onclick="this.parentElement.remove()" fill="clear" style="color: white;">
|
|
|
+ 继续发泄
|
|
|
+ </ion-button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ document.body.appendChild(message);
|
|
|
+ }
|
|
|
+}
|