HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与AR双引擎的“量子实验室“——PC端沉浸式科学实验与虚拟仿真平台

文章目录

    • 每日一句正能量
    • 前言
    • 一、前言:科学教育的交互革新需求
    • 二、核心特性解析与技术选型
      • [2.1 沉浸光感在科学实验中的价值](#2.1 沉浸光感在科学实验中的价值)
      • [2.2 Face AR + Body AR在科学实验中的协同应用](#2.2 Face AR + Body AR在科学实验中的协同应用)
    • 三、项目实战:"量子实验室"架构设计
      • [3.1 应用场景与功能规划](#3.1 应用场景与功能规划)
      • [3.2 技术架构图](#3.2 技术架构图)
    • 四、环境配置与模块依赖
      • [4.1 模块依赖配置](#4.1 模块依赖配置)
      • [4.2 权限声明(module.json5)](#4.2 权限声明(module.json5))
    • 五、核心组件实战
      • [5.1 窗口沉浸配置(LabAbility.ets)](#5.1 窗口沉浸配置(LabAbility.ets))
      • [5.2 沉浸光感标题栏(ImmersiveTitleBar.ets)](#5.2 沉浸光感标题栏(ImmersiveTitleBar.ets))
      • [5.3 Face AR + Body AR双引擎融合控制器(ARFusionController.ets)](#5.3 Face AR + Body AR双引擎融合控制器(ARFusionController.ets))
      • [5.4 虚拟实验场景(ExperimentScene.ets)](#5.4 虚拟实验场景(ExperimentScene.ets))
      • [5.5 悬浮实验导航页签(FloatTabNavigation.ets)](#5.5 悬浮实验导航页签(FloatTabNavigation.ets))
      • [5.6 多窗口光效同步管理器(WindowManager.ets)](#5.6 多窗口光效同步管理器(WindowManager.ets))
      • [5.7 浮动实验数据窗口(DataWindow.ets)](#5.7 浮动实验数据窗口(DataWindow.ets))
      • [5.8 主页面集成(LabPage.ets)](#5.8 主页面集成(LabPage.ets))
    • 六、关键技术总结
      • [6.1 沉浸光感实现清单](#6.1 沉浸光感实现清单)
      • [6.2 Face AR + Body AR双引擎实现要点](#6.2 Face AR + Body AR双引擎实现要点)
      • [6.3 PC端多窗口光效协同](#6.3 PC端多窗口光效协同)
    • 七、调试与性能优化
      • [7.1 真机调试建议](#7.1 真机调试建议)
      • [7.2 性能优化策略](#7.2 性能优化策略)
    • 八、总结与展望

每日一句正能量

愿你拥有坚如磐石的信念,还有无问东西的勇气。

知道为何而活,并有勇气一往无前。

现在的努力,辛苦,压力,承受的一切,都是为了攒够能力和本钱,去做自己更喜欢的事,去为自己争取选择的权利。

前言

摘要 :HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR & Body AR双引擎特性,为科学教育与虚拟仿真领域提供了全新的交互范式。本文将实战开发一款面向HarmonyOS PC的"量子实验室"应用,展示如何利用systemMaterialEffect打造沉浸式实验环境,通过悬浮导航实现多学科实验快速切换,基于Face AR + Body AR实现表情与姿态双驱动的虚拟实验操控,以及基于多窗口架构构建浮动实验数据、仪器面板和三维分子视图的科研协作体验。


一、前言:科学教育的交互革新需求

传统的虚拟实验室往往采用僵化的菜单导航和扁平的二维界面,在HarmonyOS PC的大屏环境下缺乏空间沉浸感和操作直观性。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation)沉浸光感(Immersive Light Effects)Face AR & Body AR双引擎特性,为科学仿真带来了"探索、沉浸、直觉化"的设计可能 。

本文核心亮点

  • 实验状态感知光效:根据当前实验类型(物理/化学/生物/天文)动态切换环境光色与安全警示色
  • 悬浮实验导航:底部悬浮页签替代传统实验菜单,支持拖拽排序与透明度调节
  • Face AR + Body AR双引擎:表情控制实验参数精度、姿态操控虚拟仪器,实现"眼观+手动"的直觉化实验
  • 多窗口科研协作:主实验窗口 + 浮动数据记录 + 仪器面板 + 三维分子视图的光效联动

二、核心特性解析与技术选型

2.1 沉浸光感在科学实验中的价值

HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为标题栏和导航组件带来细腻的光晕与反射效果 。在虚拟实验室场景中,这种材质效果能够:

  • 强化安全意识:化学实验时玻璃拟态层配合警示色光效,模拟真实实验室的安全氛围
  • 提升观察专注度:生物显微观察时动态环境光随放大倍数变化,形成"视野即焦点"的视觉引导
  • 增强数据可信度:物理测量时通过光效强弱反馈测量精度,多窗口数据对比时视觉层级清晰

2.2 Face AR + Body AR在科学实验中的协同应用

HarmonyOS 6同时支持Face AR与Body AR能力 ,在虚拟实验室中形成互补:

  • Face AR精细控制:皱眉表示需要更精确的测量、微笑确认数据有效、张嘴表示惊讶(触发异常记录)
  • Body AR直觉操作:手势抓取虚拟烧杯倾倒、手臂挥动搅拌溶液、身体前倾靠近观察显微镜
  • 双引擎融合:表情决定操作精度模式(精细/快速),姿态执行具体操作,实现"意图+动作"的自然交互

三、项目实战:"量子实验室"架构设计

3.1 应用场景与功能规划

面向HarmonyOS PC的高校虚拟实验室场景,核心功能包括:

功能模块 技术实现 沉浸光感/AR应用
主实验窗口 XComponent + 3D物理引擎 背景光效随实验安全等级变化
悬浮学科导航 HdsTabs + systemMaterialEffect 玻璃拟态页签,选中光晕反馈
Face AR精度控制 AR Engine + 表情系数 表情切换精细/快速操作模式
Body AR仪器操控 AR Engine + 骨骼跟踪 手势操控虚拟实验仪器
浮动数据记录窗口 子窗口 + Canvas 实时数据曲线光效可视化
仪器面板窗口 子窗口 + 自定义旋钮 旋钮光效随数值变化
三维分子视图 子窗口 + 3D渲染 分子结构光效渲染

3.2 技术架构图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    UI Layer (ArkUI)                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ ImmersiveTitle│  │ Experiment   │  │ FloatTabBar      │  │
│  │ Bar (HDS)     │  │ View (3D+AR) │  │ (HdsTabs)        │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│              AR Engine Layer (Face + Body)                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ Face Tracking│  │ Body Tracking│  │ Fusion Control   │  │
│  │ (Precision)  │  │ (Operation)  │  │ (Intent+Action)  │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│              Window Manager (PC Multi-Window)                │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │ Main Window  │  │ Data Win     │  │ Instrument Win   │  │
│  │ (FullScreen) │  │ (Floating)   │  │ (Floating)       │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
│  ┌──────────────┐                                            │
│  │ Molecule Win │                                            │
│  │ (Floating)   │                                            │
│  └──────────────┘                                            │
└─────────────────────────────────────────────────────────────┘

四、环境配置与模块依赖

4.1 模块依赖配置

json 复制代码
{
  "name": "quantum-lab",
  "version": "1.0.0",
  "description": "Immersive Virtual Science Lab for HarmonyOS PC",
  "dependencies": {
    "@kit.AbilityKit": "^6.1.0",
    "@kit.ArkUI": "^6.1.0",
    "@kit.UIDesignKit": "^6.1.0",
    "@kit.BasicServicesKit": "^6.1.0",
    "@kit.AREngineKit": "^6.1.0",
    "@kit.GraphicsKit": "^6.1.0",
    "@kit.SensorKit": "^6.1.0"
  }
}

4.2 权限声明(module.json5)

json 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "LabAbility",
    "deviceTypes": [
      "2in1",
      "tablet",
      "default"
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:permission_camera_reason"
      },
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:permission_mic_reason"
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:permission_internet_reason"
      }
    ]
  }
}

五、核心组件实战

5.1 窗口沉浸配置(LabAbility.ets)

typescript 复制代码
// entry/src/main/ets/ability/LabAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

export default class LabAbility extends UIAbility {
  private mainWindow: window.Window | null = null;

  onWindowStageCreate(windowStage: window.WindowStage): void {
    this.initializeLabWindow(windowStage);
  }

  private async initializeLabWindow(windowStage: window.WindowStage): Promise<void> {
    try {
      this.mainWindow = windowStage.getMainWindowSync();

      await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
      await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
      await this.mainWindow.setWindowTitleBarEnable(false);
      await this.mainWindow.setWindowLayoutFullScreen(true);
      await this.mainWindow.setWindowShadowEnabled(true);
      await this.mainWindow.setWindowCornerRadius(12);
      await this.mainWindow.setWindowBackgroundColor('#00000000');

      AppStorage.setOrCreate('main_window', this.mainWindow);

      windowStage.loadContent('pages/LabPage', (err) => {
        if (err.code) {
          console.error('Failed to load lab content:', JSON.stringify(err));
          return;
        }
        console.info('Quantum Lab main window initialized');
      });

    } catch (error) {
      console.error('Window initialization failed:', (error as BusinessError).message);
    }
  }

  onWindowStageDestroy(): void {
    this.mainWindow = null;
  }
}

5.2 沉浸光感标题栏(ImmersiveTitleBar.ets)

typescript 复制代码
// entry/src/main/ets/components/ImmersiveTitleBar.ets
import { HdsNavigation, SystemMaterialEffect } from '@kit.UIDesignKit';

export enum SafetyLevel {
  SAFE = 'safe',           // 安全-绿色
  CAUTION = 'caution',     // 注意-黄色
  WARNING = 'warning',     // 警告-橙色
  DANGER = 'danger'        // 危险-红色
}

export enum ExperimentType {
  PHYSICS = 'physics',     // 物理-蓝色
  CHEMISTRY = 'chemistry', // 化学-紫色
  BIOLOGY = 'biology',     // 生物-绿色
  ASTRONOMY = 'astronomy'  // 天文-深蓝
}

@Component
export struct ImmersiveTitleBar {
  @Prop currentExperiment: string = '单摆测重力加速度';
  @Prop experimentType: ExperimentType = ExperimentType.PHYSICS;
  @Prop safetyLevel: SafetyLevel = SafetyLevel.SAFE;
  @State isWindowFocused: boolean = true;
  @State titleBarHeight: number = 48;

  // 学科主题色映射
  private typeColors: Map<ExperimentType, string> = new Map([
    [ExperimentType.PHYSICS, '#4ECDC4'],      // 青绿-物理
    [ExperimentType.CHEMISTRY, '#9B59B6'],     // 紫色-化学
    [ExperimentType.BIOLOGY, '#27AE60'],       // 绿色-生物
    [ExperimentType.ASTRONOMY, '#2C3E50']      // 深蓝-天文
  ]);

  // 安全等级光效
  private safetyGlowColors: Map<SafetyLevel, string> = new Map([
    [SafetyLevel.SAFE, '#27C93F'],
    [SafetyLevel.CAUTION, '#F1C40F'],
    [SafetyLevel.WARNING, '#E67E22'],
    [SafetyLevel.DANGER, '#E74C3C']
  ]);

  aboutToAppear(): void {
    AppStorage.watch('window_focused', (focused: boolean) => {
      this.isWindowFocused = focused;
    });
  }

  private getThemeColor(): string {
    return this.typeColors.get(this.experimentType) || '#4ECDC4';
  }

  private getSafetyColor(): string {
    return this.safetyGlowColors.get(this.safetyLevel) || '#27C93F';
  }

  build() {
    HdsNavigation({
      title: `量子实验室 - ${this.currentExperiment}`,
      subtitle: this.getTypeText(this.experimentType),
      systemMaterialEffect: SystemMaterialEffect.IMMERSIVE,
      backgroundOpacity: this.isWindowFocused ? 0.85 : 0.55,
      height: this.titleBarHeight,
      leading: this.buildLeadingActions(),
      trailing: this.buildTrailingActions()
    })
    .width('100%')
    .border({
      width: { bottom: 2 },
      color: this.isWindowFocused 
        ? this.getThemeColor() 
        : 'rgba(255,255,255,0.1)'
    })
    .shadow({
      radius: this.isWindowFocused ? 20 : 5,
      color: this.getSafetyColor(), // 安全等级决定阴影色
      offsetX: 0,
      offsetY: 3
    })
    .animation({
      duration: 300,
      curve: Curve.EaseInOut
    })
  }

  @Builder
  buildLeadingActions(): void {
    Row({ space: 12 }) {
      // 安全等级指示灯(呼吸效果)
      Stack() {
        Circle()
          .width(16)
          .height(16)
          .fill(this.getSafetyColor())
          .opacity(0.3)
          .animation({
            duration: 1500,
            curve: Curve.EaseInOut,
            iterations: -1,
            playMode: PlayMode.Alternate
          })
          .scale({ x: 1.5, y: 1.5 })

        Circle()
          .width(12)
          .height(12)
          .fill(this.getSafetyColor())
      }
      .width(20)
      .height(20)

      // 实验控制按钮
      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_reset'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('experiment_action', 'reset');
      })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_start'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('experiment_action', 'start');
      })
    }
    .padding({ left: 16 })
  }

  @Builder
  buildTrailingActions(): void {
    Row({ space: 12 }) {
      // AR双引擎状态
      Row({ space: 4 }) {
        Circle()
          .width(6)
          .height(6)
          .fill(AppStorage.get<boolean>('face_ar_active') ? '#27C93F' : '#95A5A6')
        Circle()
          .width(6)
          .height(6)
          .fill(AppStorage.get<boolean>('body_ar_active') ? '#3498DB' : '#95A5A6')
      }

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_data'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('window_action', 'open_data');
      })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_instrument'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('window_action', 'open_instrument');
      })

      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_molecule'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF')
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(255,255,255,0.1)')
      .onClick(() => {
        AppStorage.setOrCreate('window_action', 'open_molecule');
      })
    }
    .padding({ right: 16 })
  }

  private getTypeText(type: ExperimentType): string {
    const texts: Map<ExperimentType, string> = new Map([
      [ExperimentType.PHYSICS, '物理实验'],
      [ExperimentType.CHEMISTRY, '化学实验'],
      [ExperimentType.BIOLOGY, '生物实验'],
      [ExperimentType.ASTRONOMY, '天文观测']
    ]);
    return texts.get(type) || '物理实验';
  }
}

5.3 Face AR + Body AR双引擎融合控制器(ARFusionController.ets)

核心创新组件,同时启用人脸表情跟踪和人体骨骼跟踪,实现"意图+动作"融合控制 。

typescript 复制代码
// entry/src/main/ets/components/ARFusionController.ets
import { arEngine } from '@kit.AREngineKit';
import { camera } from '@kit.CameraKit';

export enum PrecisionMode {
  FINE = 'fine',       // 精细模式(皱眉)
  NORMAL = 'normal',   // 普通模式(平静)
  FAST = 'fast'        // 快速模式(微笑)
}

export enum OperationMode {
  IDLE = 'idle',
  GRAB = 'grab',       // 抓取(握拳)
  POUR = 'pour',       // 倾倒(倾斜)
  STIR = 'stir',       // 搅拌(画圈)
  MEASURE = 'measure', // 测量(指向)
  OBSERVE = 'observe'  // 观察(靠近)
}

interface FusionCommand {
  precision: PrecisionMode;
  operation: OperationMode;
  intensity: number;     // 操作强度 0-1
  targetPosition: { x: number; y: number; z: number };
  confidence: number;
}

@Component
export struct ARFusionController {
  @State faceARActive: boolean = false;
  @State bodyARActive: boolean = false;
  @State currentPrecision: PrecisionMode = PrecisionMode.NORMAL;
  @State currentOperation: OperationMode = OperationMode.IDLE;
  @State fusionConfidence: number = 0;
  @State trackingStatus: string = '未启动';

  private faceSession: arEngine.ARSession | null = null;
  private bodySession: arEngine.ARSession | null = null;
  private faceTracker: arEngine.FaceTracker | null = null;
  private bodyTracker: arEngine.BodyTracker | null = null;
  private commandCallback: ((cmd: FusionCommand) => void) | null = null;

  aboutToAppear(): void {
    this.initializeDualAR();
  }

  aboutToDisappear(): void {
    this.releaseDualAR();
  }

  setCommandCallback(callback: (cmd: FusionCommand) => void): void {
    this.commandCallback = callback;
  }

  private async initializeDualAR(): Promise<void> {
    try {
      // 初始化Face AR(精度控制)
      this.faceSession = arEngine.createARSession({
        mode: arEngine.ARMode.FACE,
        cameraConfig: {
          cameraFacing: camera.CameraFacing.CAMERA_FACING_FRONT
        }
      });
      this.faceTracker = this.faceSession.createFaceTracker({
        maxFaceCount: 1,
        enableExpression: true,
        enablePose: true
      });
      await this.faceSession.start();
      this.faceARActive = true;
      AppStorage.setOrCreate('face_ar_active', true);

      // 初始化Body AR(操作执行)
      this.bodySession = arEngine.createARSession({
        mode: arEngine.ARMode.BODY,
        cameraConfig: {
          cameraFacing: camera.CameraFacing.CAMERA_FACING_FRONT
        }
      });
      this.bodyTracker = this.bodySession.createBodyTracker({
        maxBodyCount: 1,
        enableSkeleton: true,
        enableGesture: true
      });
      await this.bodySession.start();
      this.bodyARActive = true;
      AppStorage.setOrCreate('body_ar_active', true);

      this.trackingStatus = '双引擎运行中';
      this.startFusionTracking();
      console.info('Dual AR Fusion Controller initialized');
    } catch (error) {
      console.error('Failed to initialize Dual AR:', error);
      this.trackingStatus = '初始化失败';
    }
  }

  private startFusionTracking(): void {
    // Face AR帧处理(精度意图)
    this.faceSession?.on('frame', (frame: arEngine.ARFrame) => {
      const faces = this.faceTracker?.track(frame);
      if (faces && faces.length > 0) {
        this.currentPrecision = this.analyzePrecisionMode(faces[0]);
      }
    });

    // Body AR帧处理(操作执行)
    this.bodySession?.on('frame', (frame: arEngine.ARFrame) => {
      const bodies = this.bodyTracker?.track(frame);
      if (bodies && bodies.length > 0) {
        const operation = this.analyzeOperationMode(bodies[0]);
        const intensity = this.calculateIntensity(bodies[0]);
        const position = this.getTargetPosition(bodies[0]);

        // 融合命令生成
        const command: FusionCommand = {
          precision: this.currentPrecision,
          operation: operation,
          intensity: intensity,
          targetPosition: position,
          confidence: this.calculateFusionConfidence()
        };

        this.currentOperation = operation;
        this.fusionConfidence = command.confidence;

        if (this.commandCallback) {
          this.commandCallback(command);
        }

        AppStorage.setOrCreate('fusion_command', command);
      }
    });
  }

  private analyzePrecisionMode(face: arEngine.Face): PrecisionMode {
    const expressions = face.getExpressions();
    const smile = expressions.get(arEngine.FaceExpressionType.SMILE) || 0;
    const frown = expressions.get(arEngine.FaceExpressionType.FROWN) || 0;

    if (frown > 0.5) return PrecisionMode.FINE;      // 皱眉=精细
    if (smile > 0.5) return PrecisionMode.FAST;       // 微笑=快速
    return PrecisionMode.NORMAL;                       // 平静=普通
  }

  private analyzeOperationMode(body: arEngine.Body): OperationMode {
    const skeleton = body.getSkeleton();
    const leftHand = skeleton.getJoint(arEngine.BodyJointType.LEFT_HAND);
    const rightHand = skeleton.getJoint(arEngine.BodyJointType.RIGHT_HAND);
    const leftWrist = skeleton.getJoint(arEngine.BodyJointType.LEFT_WRIST);
    const rightWrist = skeleton.getJoint(arEngine.BodyJointType.RIGHT_WRIST);
    const nose = skeleton.getJoint(arEngine.BodyJointType.NOSE);

    // 双手靠近=抓取
    const handDistance = Math.sqrt(
      Math.pow(rightHand.x - leftHand.x, 2) + 
      Math.pow(rightHand.y - leftHand.y, 2)
    );
    if (handDistance < 0.15 && leftHand.confidence > 0.7 && rightHand.confidence > 0.7) {
      return OperationMode.GRAB;
    }

    // 单手画圈=搅拌
    if (rightWrist.confidence > 0.7 && Math.abs(rightWrist.velocityX) > 0.2 && Math.abs(rightWrist.velocityY) > 0.2) {
      return OperationMode.STIR;
    }

    // 身体前倾=观察
    if (nose.confidence > 0.7 && nose.z < -0.3) {
      return OperationMode.OBSERVE;
    }

    // 指向=测量
    if (rightHand.confidence > 0.7 && rightHand.y < rightWrist.y) {
      return OperationMode.MEASURE;
    }

    // 倾斜=倾倒
    if (leftWrist.confidence > 0.7 && Math.abs(leftWrist.velocityX) > 0.3) {
      return OperationMode.POUR;
    }

    return OperationMode.IDLE;
  }

  private calculateIntensity(body: arEngine.Body): number {
    const skeleton = body.getSkeleton();
    const rightHand = skeleton.getJoint(arEngine.BodyJointType.RIGHT_HAND);
    return Math.min(1.0, rightHand.confidence);
  }

  private getTargetPosition(body: arEngine.Body): { x: number; y: number; z: number } {
    const skeleton = body.getSkeleton();
    const rightHand = skeleton.getJoint(arEngine.BodyJointType.RIGHT_HAND);
    return { x: rightHand.x, y: rightHand.y, z: rightHand.z };
  }

  private calculateFusionConfidence(): number {
    const faceConf = this.faceARActive ? 0.5 : 0;
    const bodyConf = this.bodyARActive ? 0.5 : 0;
    return faceConf + bodyConf;
  }

  private releaseDualAR(): void {
    if (this.faceSession) {
      this.faceSession.stop();
      this.faceSession = null;
    }
    if (this.bodySession) {
      this.bodySession.stop();
      this.bodySession = null;
    }
    this.faceTracker = null;
    this.bodyTracker = null;
    this.faceARActive = false;
    this.bodyARActive = false;
    AppStorage.setOrCreate('face_ar_active', false);
    AppStorage.setOrCreate('body_ar_active', false);
    this.trackingStatus = '已停止';
  }

  build() {
    Stack() {
      // AR预览层
      Column() {
        if (this.faceARActive || this.bodyARActive) {
          XComponent({
            id: 'dual_ar_preview',
            type: XComponentType.SURFACE,
            controller: new XComponentController()
          })
            .width('100%')
            .height('100%')
            .opacity(0.15)
        }
      }
      .width('100%')
      .height('100%')

      // 融合状态可视化
      Column() {
        this.buildFusionStatusPanel()
      }
      .width('100%')
      .height('100%')

      // AR状态指示器
      this.buildARStatusIndicator()
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildFusionStatusPanel(): void {
    Column({ space: 12 }) {
      // 精度模式
      Row({ space: 8 }) {
        Text('精度模式')
          .fontSize(11)
          .fontColor('#AAAAAA')
          .width(60)

        Row({ space: 4 }) {
          ForEach([PrecisionMode.FINE, PrecisionMode.NORMAL, PrecisionMode.FAST], (mode) => {
            Column()
              .width(24)
              .height(24)
              .backgroundColor(this.currentPrecision === mode 
                ? (mode === PrecisionMode.FINE ? '#FF6B6B' : 
                   mode === PrecisionMode.NORMAL ? '#4ECDC4' : '#FFEAA7')
                : 'rgba(255,255,255,0.1)')
              .borderRadius(12)
              .border({
                width: this.currentPrecision === mode ? 2 : 0,
                color: '#FFFFFF'
              })
          })
        }
      }

      // 操作模式
      Text(this.currentOperation !== OperationMode.IDLE ? `操作: ${this.getOperationText()}` : '等待操作...')
        .fontSize(14)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      // 融合置信度
      Progress({
        value: this.fusionConfidence * 100,
        total: 100,
        type: ProgressType.Linear
      })
        .width(120)
        .height(4)
        .color('#4ECDC4')
    }
    .width(180)
    .height('auto')
    .padding(16)
    .backgroundColor('rgba(0,0,0,0.6)')
    .borderRadius(16)
    .position({ x: '85%', y: '25%' })
    .anchor('100% 0%')
    .backdropFilter($r('sys.blur.10'))
  }

  @Builder
  buildARStatusIndicator(): void {
    Row({ space: 6 }) {
      Circle()
        .width(8)
        .height(8)
        .fill(this.faceARActive && this.bodyARActive ? '#27C93F' : '#95A5A6')
        .shadow({
          radius: 6,
          color: this.faceARActive && this.bodyARActive ? '#27C93F' : 'transparent'
        })

      Text(`AR Fusion · ${this.trackingStatus}`)
        .fontSize(11)
        .fontColor(this.faceARActive && this.bodyARActive ? '#27C93F' : '#95A5A6')
    }
    .width('auto')
    .height(28)
    .padding({ left: 10, right: 10 })
    .backgroundColor('rgba(0,0,0,0.6)')
    .borderRadius(14)
    .position({ x: 16, y: 16 })
    .backdropFilter($r('sys.blur.10'))
  }

  private getOperationText(): string {
    const texts: Map<OperationMode, string> = new Map([
      [OperationMode.GRAB, '抓取'],
      [OperationMode.POUR, '倾倒'],
      [OperationMode.STIR, '搅拌'],
      [OperationMode.MEASURE, '测量'],
      [OperationMode.OBSERVE, '观察']
    ]);
    return texts.get(this.currentOperation) || '未知';
  }
}

5.4 虚拟实验场景(ExperimentScene.ets)

typescript 复制代码
// entry/src/main/ets/components/ExperimentScene.ets
import { FusionCommand, PrecisionMode, OperationMode } from './ARFusionController';

@Component
export struct ExperimentScene {
  @Prop experimentType: string = 'physics';
  @Prop themeColor: string = '#4ECDC4';
  @State beakerAngle: number = 0;
  @State liquidLevel: number = 50;
  @State stirSpeed: number = 0;
  @State measureValue: number = 0;
  @State isGrabbing: boolean = false;
  @State grabPosition: { x: number; y: number } = { x: 0, y: 0 };

  aboutToAppear(): void {
    AppStorage.watch('fusion_command', (cmd: FusionCommand) => {
      this.executeCommand(cmd);
    });
  }

  private executeCommand(cmd: FusionCommand): void {
    const precisionFactor = cmd.precision === PrecisionMode.FINE ? 0.2 : 
                           cmd.precision === PrecisionMode.FAST ? 2.0 : 1.0;

    switch (cmd.operation) {
      case OperationMode.GRAB:
        this.isGrabbing = true;
        this.grabPosition = { x: cmd.targetPosition.x * 100, y: cmd.targetPosition.y * 100 };
        break;
      case OperationMode.POUR:
        this.beakerAngle = Math.min(90, this.beakerAngle + 2 * precisionFactor * cmd.intensity);
        this.liquidLevel = Math.max(0, this.liquidLevel - 1 * precisionFactor);
        break;
      case OperationMode.STIR:
        this.stirSpeed = Math.min(100, this.stirSpeed + 5 * precisionFactor);
        // 搅拌后逐渐减速
        setTimeout(() => { this.stirSpeed = Math.max(0, this.stirSpeed - 10); }, 500);
        break;
      case OperationMode.MEASURE:
        this.measureValue = 9.8 + (Math.random() - 0.5) * 0.2 * precisionFactor;
        break;
      case OperationMode.OBSERVE:
        // 放大观察
        break;
      case OperationMode.IDLE:
        this.isGrabbing = false;
        this.beakerAngle = Math.max(0, this.beakerAngle - 1);
        break;
    }
  }

  build() {
    Stack() {
      // 实验台背景
      this.buildLabBenchBackground()

      // 虚拟仪器层
      Column() {
        // 烧杯
        this.buildBeaker()

        // 搅拌棒
        if (this.stirSpeed > 0) {
          this.buildStirringRod()
        }

        // 测量仪器
        this.buildMeasurementDevice()
      }
      .width('100%')
      .height('100%')
      .position({ x: '50%', y: '50%' })
      .anchor('50%')

      // 抓取指示器
      if (this.isGrabbing) {
        this.buildGrabIndicator()
      }

      // 数据浮层
      this.buildDataOverlay()
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildLabBenchBackground(): void {
    Column() {
      // 实验台桌面
      Column()
        .width('100%')
        .height('40%')
        .position({ x: 0, y: '60%' })
        .linearGradient({
          direction: GradientDirection.Top,
          colors: [
            ['#2C3E50', 0.0],
            ['#34495E', 1.0]
          ]
        })

      // 背景光效
      Column()
        .width(600)
        .height(600)
        .backgroundColor(this.themeColor)
        .blur(200)
        .opacity(0.08)
        .position({ x: '50%', y: '40%' })
        .anchor('50%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }

  @Builder
  buildBeaker(): void {
    Stack() {
      // 烧杯主体
      Column()
        .width(80)
        .height(120)
        .backgroundColor('rgba(200,220,255,0.2)')
        .border({
          width: 2,
          color: 'rgba(255,255,255,0.3)'
        })
        .borderRadius({ bottomLeft: 8, bottomRight: 8 })

      // 液体
      Column()
        .width(76)
        .height(this.liquidLevel * 1.2)
        .backgroundColor(this.getLiquidColor())
        .opacity(0.7)
        .borderRadius({ bottomLeft: 6, bottomRight: 6 })
        .position({ x: 2, y: 120 - this.liquidLevel * 1.2 })
        .animation({
          duration: 300,
          curve: Curve.EaseInOut
        })

      // 刻度线
      ForEach([25, 50, 75, 100], (mark) => {
        Column()
          .width(20)
          .height(1)
          .backgroundColor('rgba(255,255,255,0.3)')
          .position({ x: 5, y: 120 - mark * 1.2 })
      })
    }
    .width(80)
    .height(120)
    .rotate({ x: 0, y: 0, z: 1, angle: this.beakerAngle })
    .animation({
      duration: 300,
      curve: Curve.EaseInOut
    })
  }

  @Builder
  buildStirringRod(): void {
    Column()
      .width(4)
      .height(100)
      .backgroundColor('rgba(255,255,255,0.5)')
      .borderRadius(2)
      .position({ x: 40, y: 10 })
      .rotate({
        x: 0,
        y: 0,
        z: 1,
        angle: Math.sin(Date.now() / 100) * 15 * (this.stirSpeed / 50)
      })
      .animation({
        duration: 100,
        curve: Curve.Linear,
        iterations: -1
      })
  }

  @Builder
  buildMeasurementDevice(): void {
    Column({ space: 4 }) {
      Text(`${this.measureValue.toFixed(2)}`)
        .fontSize(24)
        .fontColor('#4ECDC4')
        .fontWeight(FontWeight.Bold)
        .fontFamily('monospace')

      Text('m/s²')
        .fontSize(12)
        .fontColor('#AAAAAA')
    }
    .width(120)
    .height(80)
    .backgroundColor('rgba(0,0,0,0.6)')
    .borderRadius(12)
    .border({
      width: 1,
      color: '#4ECDC4'
    })
    .position({ x: 120, y: 0 })
    .backdropFilter($r('sys.blur.10'))
  }

  @Builder
  buildGrabIndicator(): void {
    Circle()
      .width(30)
      .height(30)
      .fill('rgba(255,255,255,0.3)')
      .border({
        width: 2,
        color: '#FFFFFF'
      })
      .position({ x: this.grabPosition.x, y: this.grabPosition.y })
      .animation({
        duration: 200,
        curve: Curve.EaseOut
      })
  }

  @Builder
  buildDataOverlay(): void {
    Column({ space: 8 }) {
      Text(`液位: ${Math.round(this.liquidLevel)}%`)
        .fontSize(11)
        .fontColor('#AAAAAA')
      Text(`角度: ${Math.round(this.beakerAngle)}°`)
        .fontSize(11)
        .fontColor('#AAAAAA')
      Text(`搅拌: ${Math.round(this.stirSpeed)} RPM`)
        .fontSize(11)
        .fontColor('#AAAAAA')
    }
    .width('auto')
    .height('auto')
    .padding(12)
    .backgroundColor('rgba(0,0,0,0.5)')
    .borderRadius(10)
    .position({ x: 16, y: '85%' })
    .backdropFilter($r('sys.blur.10'))
  }

  private getLiquidColor(): string {
    const colors: Map<string, string> = new Map([
      ['physics', '#4ECDC4'],
      ['chemistry', '#9B59B6'],
      ['biology', '#27AE60'],
      ['astronomy', '#3498DB']
    ]);
    return colors.get(this.experimentType) || '#4ECDC4';
  }
}

5.5 悬浮实验导航页签(FloatTabNavigation.ets)

typescript 复制代码
// entry/src/main/ets/components/FloatTabNavigation.ets
import { window } from '@kit.ArkUI';
import { HdsTabs, SystemMaterialEffect } from '@kit.UIDesignKit';

export enum TransparencyLevel {
  STRONG = 0.85,
  BALANCED = 0.70,
  WEAK = 0.55
}

interface ExperimentTab {
  id: string;
  name: string;
  theme: string;
  subject: string;
  difficulty: string;
  icon: Resource;
}

@Component
export struct FloatTabNavigation {
  @Prop currentIndex: number = 0;
  @State navTransparency: number = TransparencyLevel.BALANCED;
  @State isExpanded: boolean = false;
  @State bottomAvoidHeight: number = 0;
  @State tabs: ExperimentTab[] = [
    { id: '1', name: '单摆测g', theme: '#4ECDC4', subject: '物理', difficulty: '基础', icon: $r('app.media.ic_physics') },
    { id: '2', name: '酸碱滴定', theme: '#9B59B6', subject: '化学', difficulty: '中等', icon: $r('app.media.ic_chemistry') },
    { id: '3', name: '细胞观察', theme: '#27AE60', subject: '生物', difficulty: '基础', icon: $r('app.media.ic_biology') },
    { id: '4', name: '光谱分析', theme: '#2C3E50', subject: '天文', difficulty: '高级', icon: $r('app.media.ic_astronomy') },
    { id: '5', name: '电路仿真', theme: '#E67E22', subject: '物理', difficulty: '中等', icon: $r('app.media.ic_circuit') }
  ];

  aboutToAppear(): void {
    this.getBottomAvoidArea();
  }

  private async getBottomAvoidArea(): Promise<void> {
    try {
      const mainWindow = await window.getLastWindow();
      const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      this.bottomAvoidHeight = avoidArea.bottomRect.height;
    } catch (error) {
      console.error('Failed to get avoid area:', error);
    }
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column() {
        this.contentBuilder()
      }
      .padding({ bottom: this.bottomAvoidHeight + 88 })

      Column() {
        Stack() {
          Column()
            .width('100%')
            .height('100%')
            .backgroundBlurStyle(BlurStyle.REGULAR)
            .opacity(this.navTransparency)
            .backdropFilter($r('sys.blur.20'))

          Column()
            .width('100%')
            .height('100%')
            .linearGradient({
              direction: GradientDirection.Top,
              colors: [
                ['rgba(255,255,255,0.15)', 0.0],
                ['rgba(255,255,255,0.05)', 1.0]
              ]
            })
        }
        .width('100%')
        .height('100%')
        .borderRadius(20)
        .shadow({
          radius: 20,
          color: 'rgba(0,0,0,0.2)',
          offsetX: 0,
          offsetY: -4
        })

        Row() {
          ForEach(this.tabs, (tab: ExperimentTab, index: number) => {
            this.buildExperimentTab(tab, index)
          }, (tab: ExperimentTab) => tab.id)
        }
        .width('100%')
        .height(64)
        .padding({ left: 16, right: 16 })
        .justifyContent(FlexAlign.Start)

        if (this.isExpanded) {
          this.buildTransparencyPanel()
        }
      }
      .width('96%')
      .height(this.isExpanded ? 108 : 64)
      .margin({ 
        bottom: this.bottomAvoidHeight + 12, 
        left: '2%', 
        right: '2%' 
      })
      .animation({
        duration: 300,
        curve: Curve.Spring,
        iterations: 1
      })
      .gesture(
        LongPressGesture({ duration: 500 })
          .onAction(() => {
            this.isExpanded = !this.isExpanded;
          })
      )
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildExperimentTab(tab: ExperimentTab, index: number): void {
    Row({ space: 6 }) {
      Image(tab.icon)
        .width(16)
        .height(16)
        .fillColor(tab.theme)

      Text(tab.name)
        .fontSize(13)
        .fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
        .fontColor(this.currentIndex === index ? '#FFFFFF' : '#AAAAAA')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .width(80)

      Text(`${tab.subject} · ${tab.difficulty}`)
        .fontSize(10)
        .fontColor('#666666')
        .backgroundColor('rgba(255,255,255,0.1)')
        .borderRadius(8)
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })

      if (this.currentIndex === index) {
        Button({ type: ButtonType.Circle }) {
          Text('×')
            .fontSize(14)
            .fontColor('#AAAAAA')
        }
        .width(20)
        .height(20)
        .backgroundColor('transparent')
        .onClick(() => {
          this.closeTab(index);
        })
      }
    }
    .height(40)
    .padding({ left: 12, right: 12 })
    .backgroundColor(this.currentIndex === index 
      ? 'rgba(255,255,255,0.15)' 
      : 'transparent')
    .borderRadius(12)
    .border({
      width: this.currentIndex === index ? 1 : 0,
      color: tab.theme
    })
    .onClick(() => {
      this.currentIndex = index;
      AppStorage.setOrCreate('current_experiment', tab.name);
      AppStorage.setOrCreate('current_subject', tab.subject);
      AppStorage.setOrCreate('current_theme', tab.theme);
    })
  }

  @Builder
  buildTransparencyPanel(): void {
    Row({ space: 12 }) {
      Text('透明度')
        .fontSize(12)
        .fontColor('#AAAAAA')

      Slider({
        value: this.navTransparency * 100,
        min: 55,
        max: 85,
        step: 15,
        style: SliderStyle.InSet
      })
        .width(120)
        .onChange((value: number) => {
          this.navTransparency = value / 100;
        })

      Text(`${Math.round(this.navTransparency * 100)}%`)
        .fontSize(12)
        .fontColor('#AAAAAA')

      Button('强')
        .fontSize(11)
        .backgroundColor(this.navTransparency === TransparencyLevel.STRONG 
          ? '#4ECDC4' 
          : 'rgba(255,255,255,0.1)')
        .onClick(() => { this.navTransparency = TransparencyLevel.STRONG; })

      Button('平衡')
        .fontSize(11)
        .backgroundColor(this.navTransparency === TransparencyLevel.BALANCED 
          ? '#4ECDC4' 
          : 'rgba(255,255,255,0.1)')
        .onClick(() => { this.navTransparency = TransparencyLevel.BALANCED; })

      Button('弱')
        .fontSize(11)
        .backgroundColor(this.navTransparency === TransparencyLevel.WEAK 
          ? '#4ECDC4' 
          : 'rgba(255,255,255,0.1)')
        .onClick(() => { this.navTransparency = TransparencyLevel.WEAK; })
    }
    .width('100%')
    .height(44)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius({ topLeft: 12, topRight: 12 })
  }

  private closeTab(index: number): void {
    if (this.tabs.length <= 1) return;
    this.tabs.splice(index, 1);
    if (this.currentIndex >= index && this.currentIndex > 0) {
      this.currentIndex--;
    }
  }

  @BuilderParam contentBuilder: () => void = this.defaultContentBuilder;

  @Builder
  defaultContentBuilder(): void {
    Column() {
      Text('实验区域')
        .fontSize(16)
        .fontColor('#999999')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

5.6 多窗口光效同步管理器(WindowManager.ets)

typescript 复制代码
// entry/src/main/ets/utils/WindowManager.ets
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

export interface ToolWindowConfig {
  name: string;
  title: string;
  width: number;
  height: number;
  x?: number;
  y?: number;
  followMainWindow?: boolean;
  themeColor?: string;
}

export class WindowManager {
  private static instance: WindowManager;
  private mainWindow: window.Window | null = null;
  private subWindows: Map<string, window.Window> = new Map();

  static getInstance(): WindowManager {
    if (!WindowManager.instance) {
      WindowManager.instance = new WindowManager();
    }
    return WindowManager.instance;
  }

  async initializeMainWindow(windowStage: window.WindowStage): Promise<void> {
    this.mainWindow = windowStage.getMainWindowSync();
    await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
    await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
    await this.mainWindow.setWindowTitleBarEnable(false);
    await this.mainWindow.setWindowShadowEnabled(true);
    await this.mainWindow.setWindowCornerRadius(12);
    await this.mainWindow.setWindowBackgroundColor('#00000000');

    this.mainWindow.on('windowFocusChange', (isFocused: boolean) => {
      AppStorage.setOrCreate('window_focused', isFocused);
      if (isFocused) {
        this.syncGlobalLightEffect(AppStorage.get<string>('global_theme_color') || '#4ECDC4');
      }
    });

    console.info('Main window initialized for Quantum Lab');
  }

  async createToolWindow(config: ToolWindowConfig): Promise<window.Window | null> {
    try {
      if (!this.mainWindow) {
        throw new Error('Main window not initialized');
      }

      const subWindow = await this.mainWindow.createSubWindow(config.name);
      await subWindow.setWindowSizeType(window.WindowSizeType.FREE);
      await subWindow.moveWindowTo({ x: config.x ?? 100, y: config.y ?? 100 });
      await subWindow.resize(config.width, config.height);
      await subWindow.setWindowBackgroundColor('#00000000');
      await subWindow.setWindowShadowEnabled(true);
      await subWindow.setWindowCornerRadius(16);
      await subWindow.setWindowTopmost(true);

      this.subWindows.set(config.name, subWindow);
      await subWindow.setUIContent(`pages/${config.name}`);
      await subWindow.showWindow();

      if (config.followMainWindow) {
        this.setupWindowFollow(subWindow, config);
      }

      this.syncSubWindowLightEffect(subWindow, config.name, config.themeColor);
      return subWindow;
    } catch (error) {
      console.error(`Failed to create tool window:`, (error as BusinessError).message);
      return null;
    }
  }

  private setupWindowFollow(subWindow: window.Window, config: ToolWindowConfig): void {
    this.mainWindow?.on('windowRectChange', (data: window.RectChangeOptions) => {
      if (data.rectChangeReason === window.RectChangeReason.MOVE) {
        const mainRect = this.mainWindow?.getWindowProperties().windowRect;
        if (mainRect) {
          subWindow.moveWindowTo({
            x: mainRect.left + (config.x ?? 100),
            y: mainRect.top + (config.y ?? 100)
          });
        }
      }
    });
  }

  private syncSubWindowLightEffect(subWindow: window.Window, name: string, themeColor?: string): void {
    subWindow.on('windowFocusChange', (isFocused: boolean) => {
      AppStorage.setOrCreate(`window_${name}_focused`, isFocused);
      if (isFocused && themeColor) {
        AppStorage.setOrCreate('global_theme_color', themeColor);
      }
    });

    AppStorage.watch('global_theme_color', (color: string) => {
      console.info(`Syncing theme color ${color} to window ${name}`);
    });
  }

  async syncGlobalLightEffect(color: string): Promise<void> {
    AppStorage.setOrCreate('global_theme_color', color);
  }

  async openDataWindow(): Promise<void> {
    await this.createToolWindow({
      name: 'DataWindow',
      title: '实验数据',
      width: 500,
      height: 400,
      x: 1000,
      y: 100,
      themeColor: '#4ECDC4'
    });
  }

  async openInstrumentWindow(): Promise<void> {
    await this.createToolWindow({
      name: 'InstrumentWindow',
      title: '仪器面板',
      width: 400,
      height: 500,
      x: 200,
      y: 100,
      themeColor: '#9B59B6'
    });
  }

  async openMoleculeWindow(): Promise<void> {
    await this.createToolWindow({
      name: 'MoleculeWindow',
      title: '三维分子',
      width: 600,
      height: 500,
      x: 50,
      y: 500,
      themeColor: '#27AE60'
    });
  }

  async closeToolWindow(name: string): Promise<void> {
    const subWindow = this.subWindows.get(name);
    if (subWindow) {
      await subWindow.destroyWindow();
      this.subWindows.delete(name);
    }
  }
}

5.7 浮动实验数据窗口(DataWindow.ets)

typescript 复制代码
// entry/src/main/ets/pages/DataWindow.ets
@Entry
@Component
struct DataWindow {
  @State dataPoints: Array<{time: number, value: number}> = [];
  @State isRecording: boolean = false;
  @State isFocused: boolean = false;
  @State themeColor: string = '#4ECDC4';

  aboutToAppear(): void {
    AppStorage.watch('window_DataWindow_focused', (focused: boolean) => {
      this.isFocused = focused;
    });
    AppStorage.watch('current_theme', (color: string) => {
      this.themeColor = color;
    });

    // 模拟数据生成
    this.startDataSimulation();
  }

  private startDataSimulation(): void {
    let time = 0;
    setInterval(() => {
      if (this.isRecording) {
        time += 0.1;
        const value = 9.8 + Math.sin(time) * 0.3 + (Math.random() - 0.5) * 0.1;
        this.dataPoints.push({ time, value });
        if (this.dataPoints.length > 100) {
          this.dataPoints.shift();
        }
      }
    }, 100);
  }

  build() {
    Stack() {
      Column() {
        Column()
          .width(400)
          .height(400)
          .backgroundColor(this.themeColor)
          .blur(150)
          .opacity(this.isFocused ? 0.1 : 0.05)
          .position({ x: '50%', y: '30%' })
          .anchor('50%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#0f0f1a')

      Column() {
        Row() {
          Text('实验数据')
            .fontSize(14)
            .fontColor(this.themeColor)
            .fontWeight(FontWeight.Bold)
          
          Row({ space: 8 }) {
            Circle().width(12).height(12).fill('#FF5F56')
            Circle().width(12).height(12).fill('#FFBD2E')
            Circle().width(12).height(12).fill('#27C93F')
          }
        }
        .width('100%')
        .height(36)
        .padding({ left: 16, right: 16 })
        .justifyContent(FlexAlign.SpaceBetween)
        .backgroundColor(`rgba(${this.hexToRgb(this.themeColor)},0.05)`)

        // 数据图表
        Canvas(this.context)
          .width('100%')
          .height(200)
          .backgroundColor('rgba(0,0,0,0.3)')
          .borderRadius(8)
          .onReady((context: CanvasRenderingContext2D) => {
            this.drawDataCurve(context);
          })

        // 统计信息
        Row({ space: 16 }) {
          Column() {
            Text('平均值')
              .fontSize(11)
              .fontColor('#AAAAAA')
            Text(`${this.getAverage().toFixed(3)}`)
              .fontSize(16)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }

          Column() {
            Text('标准差')
              .fontSize(11)
              .fontColor('#AAAAAA')
            Text(`${this.getStdDev().toFixed(3)}`)
              .fontSize(16)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }

          Column() {
            Text('数据点')
              .fontSize(11)
              .fontColor('#AAAAAA')
            Text(`${this.dataPoints.length}`)
              .fontSize(16)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
        }
        .width('100%')
        .height(60)
        .justifyContent(FlexAlign.SpaceEvenly)

        // 控制按钮
        Row({ space: 12 }) {
          Button(this.isRecording ? '停止记录' : '开始记录')
            .type(ButtonType.Capsule)
            .fontSize(13)
            .backgroundColor(this.isRecording ? '#FF6B6B' : this.themeColor)
            .width(120)
            .onClick(() => {
              this.isRecording = !this.isRecording;
            })

          Button('导出CSV')
            .type(ButtonType.Capsule)
            .fontSize(13)
            .backgroundColor('rgba(255,255,255,0.1)')
            .width(120)
        }
        .width('100%')
        .height(56)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }

  private drawDataCurve(context: CanvasRenderingContext2D): void {
    const width = 480;
    const height = 200;
    context.clearRect(0, 0, width, height);

    if (this.dataPoints.length < 2) return;

    // 绘制网格
    context.strokeStyle = 'rgba(255,255,255,0.1)';
    context.lineWidth = 1;
    for (let i = 0; i < 5; i++) {
      const y = (height / 4) * i;
      context.beginPath();
      context.moveTo(0, y);
      context.lineTo(width, y);
      context.stroke();
    }

    // 绘制数据曲线
    context.strokeStyle = this.themeColor;
    context.lineWidth = 2;
    context.beginPath();

    const minTime = this.dataPoints[0].time;
    const maxTime = this.dataPoints[this.dataPoints.length - 1].time;
    const timeRange = maxTime - minTime || 1;

    this.dataPoints.forEach((point, index) => {
      const x = ((point.time - minTime) / timeRange) * width;
      const y = height - ((point.value - 8) / 4) * height;
      if (index === 0) {
        context.moveTo(x, y);
      } else {
        context.lineTo(x, y);
      }
    });
    context.stroke();

    // 绘制数据点
    this.dataPoints.forEach((point) => {
      const x = ((point.time - minTime) / timeRange) * width;
      const y = height - ((point.value - 8) / 4) * height;
      context.fillStyle = this.themeColor;
      context.beginPath();
      context.arc(x, y, 3, 0, Math.PI * 2);
      context.fill();
    });
  }

  private getAverage(): number {
    if (this.dataPoints.length === 0) return 0;
    const sum = this.dataPoints.reduce((acc, p) => acc + p.value, 0);
    return sum / this.dataPoints.length;
  }

  private getStdDev(): number {
    if (this.dataPoints.length < 2) return 0;
    const avg = this.getAverage();
    const variance = this.dataPoints.reduce((acc, p) => acc + Math.pow(p.value - avg, 2), 0);
    return Math.sqrt(variance / this.dataPoints.length);
  }

  private hexToRgb(hex: string): string {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? 
      `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` 
      : '78,205,196';
  }
}

5.8 主页面集成(LabPage.ets)

typescript 复制代码
// entry/src/main/ets/pages/LabPage.ets
import { ImmersiveTitleBar, ExperimentType, SafetyLevel } from '../components/ImmersiveTitleBar';
import { FloatTabNavigation } from '../components/FloatTabNavigation';
import { ExperimentScene } from '../components/ExperimentScene';
import { ARFusionController } from '../components/ARFusionController';
import { WindowManager } from '../utils/WindowManager';

@Entry
@Component
struct LabPage {
  @State currentExperiment: number = 0;
  @State currentSubject: string = 'physics';
  @State currentTheme: string = '#4ECDC4';
  @State safetyLevel: SafetyLevel = SafetyLevel.SAFE;
  @State useDualAR: boolean = true;
  @State lightIntensity: number = 0.6;

  aboutToAppear(): void {
    AppStorage.watch('current_experiment', (exp: string) => {
      // 实验切换逻辑
    });

    AppStorage.watch('experiment_action', (action: string) => {
      if (action === 'reset') {
        // 重置实验
      } else if (action === 'start') {
        // 开始实验
      }
    });

    AppStorage.watch('window_action', (action: string) => {
      if (action === 'open_data') {
        WindowManager.getInstance().openDataWindow();
      } else if (action === 'open_instrument') {
        WindowManager.getInstance().openInstrumentWindow();
      } else if (action === 'open_molecule') {
        WindowManager.getInstance().openMoleculeWindow();
      }
    });
  }

  private getSubjectTheme(subject: string): string {
    const themes: Map<string, string> = new Map([
      ['物理', '#4ECDC4'],
      ['化学', '#9B59B6'],
      ['生物', '#27AE60'],
      ['天文', '#2C3E50']
    ]);
    return themes.get(subject) || '#4ECDC4';
  }

  build() {
    Stack() {
      this.buildAmbientLightLayer()

      Column() {
        ImmersiveTitleBar({ 
          currentExperiment: this.getExperimentName(this.currentExperiment),
          experimentType: this.getExperimentType(this.currentSubject),
          safetyLevel: this.safetyLevel 
        })

        Stack() {
          ExperimentScene({
            experimentType: this.currentSubject,
            themeColor: this.currentTheme
          })
            .width('100%')
            .height('100%')

          if (this.useDualAR) {
            ARFusionController()
              .width('100%')
              .height('100%')
              .position({ x: 0, y: 0 })
          }
        }
        .layoutWeight(1)
      }
      .width('100%')
      .height('100%')

      FloatTabNavigation({
        currentIndex: this.currentExperiment,
        onTabChange: (index: number) => {
          this.currentExperiment = index;
          this.currentSubject = this.getExperimentSubject(index);
          this.currentTheme = this.getSubjectTheme(this.currentSubject);
          WindowManager.getInstance().syncGlobalLightEffect(this.currentTheme);
        },
        contentBuilder: () => {}
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a0f')
    .expandSafeArea(
      [SafeAreaType.SYSTEM],
      [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END]
    )
  }

  private getExperimentName(index: number): string {
    const names = ['单摆测重力加速度', '酸碱滴定', '细胞观察', '光谱分析', '电路仿真'];
    return names[index] || '单摆测重力加速度';
  }

  private getExperimentSubject(subject: string): ExperimentType {
    const types: Map<string, ExperimentType> = new Map([
      ['物理', ExperimentType.PHYSICS],
      ['化学', ExperimentType.CHEMISTRY],
      ['生物', ExperimentType.BIOLOGY],
      ['天文', ExperimentType.ASTRONOMY]
    ]);
    return types.get(subject) || ExperimentType.PHYSICS;
  }

  private getExperimentSubject(index: number): string {
    const subjects = ['物理', '化学', '生物', '天文', '物理'];
    return subjects[index] || '物理';
  }

  @Builder
  buildAmbientLightLayer(): void {
    Column() {
      Column()
        .width(600)
        .height(600)
        .backgroundColor(this.currentTheme)
        .blur(180)
        .opacity(this.lightIntensity * 0.3)
        .position({ x: '50%', y: '25%' })
        .anchor('50%')
        .animation({
          duration: 7000,
          curve: Curve.EaseInOut,
          iterations: -1,
          playMode: PlayMode.Alternate
        })
        .scale({ x: 1.4, y: 1.4 })

      Column()
        .width('100%')
        .height(250)
        .backgroundColor(this.currentTheme)
        .opacity(this.lightIntensity * 0.08)
        .blur(120)
        .position({ x: 0, y: '75%' })
        .linearGradient({
          direction: GradientDirection.Top,
          colors: [
            [this.currentTheme, 0.0],
            ['transparent', 1.0]
          ]
        })

      // 安全警示光效(危险时闪烁)
      if (this.safetyLevel === SafetyLevel.DANGER) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(231,76,60,0.1)')
          .animation({
            duration: 500,
            curve: Curve.EaseInOut,
            iterations: -1,
            playMode: PlayMode.Alternate
          })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#050508')
  }
}

六、关键技术总结

6.1 沉浸光感实现清单

技术点 API/方法 应用场景
系统材质效果 systemMaterialEffect: SystemMaterialEffect.IMMERSIVE HdsNavigation标题栏
背景模糊 backgroundBlurStyle(BlurStyle.REGULAR) 悬浮导航玻璃拟态
背景滤镜 backdropFilter($r('sys.blur.20')) 精细模糊控制
安全区扩展 expandSafeArea([SafeAreaType.SYSTEM], [...]) 全屏沉浸布局
窗口沉浸 setWindowLayoutFullScreen(true) 无边框模式
光效动画 animation({ duration, iterations: -1 }) 呼吸灯/安全警示
动态透明度 backgroundOpacity 焦点感知降级

6.2 Face AR + Body AR双引擎实现要点

技术点 API/方法 说明
Face AR会话 arEngine.createARSession({ mode: ARMode.FACE }) 精度意图识别
Body AR会话 arEngine.createARSession({ mode: ARMode.BODY }) 操作执行识别
表情系数 expressions.get(FaceExpressionType.FROWN) 皱眉=精细模式
骨骼关节 skeleton.getJoint(BodyJointType.LEFT_HAND) 手部位置跟踪
融合命令 自定义FusionCommand接口 意图+动作融合
双帧同步 并行处理Face+Body帧 实时融合输出

6.3 PC端多窗口光效协同

  • 主窗口:全屏沉浸,安全等级决定警示光效
  • 浮动工具窗口:置顶、圆角、阴影,跟随主窗口移动
  • 光效同步 :通过AppStorage全局状态实现跨窗口主题色联动
  • 焦点感知:窗口激活时边缘发光增强,失活时自动降低光效强度

七、调试与性能优化

7.1 真机调试建议

  1. 双AR引擎性能:同时运行Face AR和Body AR对GPU要求较高,建议使用高性能PC真机
  2. 手势识别校准:不同实验场景需要调整手势识别阈值,建议提供校准界面
  3. 安全警示测试:验证危险状态时的红色闪烁光效是否足够醒目

7.2 性能优化策略

typescript 复制代码
// 1. 双AR引擎性能优化
private optimizeDualAR(): void {
  if (this.faceSession) {
    this.faceSession.setCameraConfig({
      fps: 10,
      resolution: { width: 320, height: 240 }
    });
  }
  if (this.bodySession) {
    this.bodySession.setCameraConfig({
      fps: 15,
      resolution: { width: 480, height: 360 }
    });
  }
}

// 2. 3D实验场景优化
aboutToDisappear(): void {
  this.pausePhysicsSimulation = true;
}

// 3. 窗口创建优化
private async lazyLoadToolWindows(): Promise<void> {
  if (!this.dataWindow) {
    this.dataWindow = await WindowManager.getInstance().openDataWindow();
  }
}

八、总结与展望

本文基于HarmonyOS 6(API 23)的悬浮导航沉浸光感Face AR + Body AR双引擎特性,完整实战了一款面向PC端的"量子实验室"沉浸式科学实验与虚拟仿真平台。核心创新点总结:

  1. 学科感知光效+安全警示系统:根据实验学科动态切换主题色,安全等级实时反馈警示光效,营造真实实验室的安全氛围

  2. Face AR + Body AR双引擎融合:Face AR识别表情意图(精细/快速模式),Body AR执行具体操作(抓取/倾倒/搅拌),实现"意图+动作"的自然交互

  3. 悬浮实验导航:底部悬浮页签替代传统实验菜单,玻璃拟态设计+三档透明度调节,在保持导航可达性的同时最大化实验区域

  4. PC级多窗口科研 :主实验窗口 + 浮动数据记录 + 仪器面板 + 三维分子视图的四层架构,通过WindowManager实现跨窗口光效联动与焦点感知

未来扩展方向

  • 接入分布式软总线,实现跨设备协同实验(手机AR观察、平板数据记录、PC主控仿真)
  • AI实验助手:基于当前实验状态,AI推荐下一步操作并预警潜在危险
  • VR/AR融合实验:支持VR头显接入,实现完全沉浸式的分子级微观实验
  • 远程实验协作:多人实时在线协作实验,通过AR同步操作意图

转载自:https://blog.csdn.net/u014727709/article/details/138339290

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
nashane17 小时前
HarmonyOS Video组件预览图片优化实践:告别黑屏,提升视频播放体验
华为·音视频·harmonyos·harmonyos 5
maaath21 小时前
【maaath】Flutter for OpenHarmony 实战:旅游攻略应用开发指南
flutter·华为·harmonyos
三声三视1 天前
ArkTS 性能优化实战:从卡顿分析到高帧率应用全攻略
华为·性能优化·harmonyos·鸿蒙
小雨青年1 天前
鸿蒙 HarmonyOS 6 | PDFKit预览能力升级实战
华为·harmonyos
花先锋队长1 天前
鸿蒙6.1加持菜鸟App:地理围栏+实况窗,靠近驿站自动提醒,取件不再遗漏
华为·智能手机·harmonyos
nashane1 天前
HarmonyOS 6学习:页面跳转弹窗状态保持全解析
学习·华为·harmonyos·harmonyos 5
maaath1 天前
【maaath】Flutter for OpenHarmony 实战:电影榜单应用开发指南
flutter·华为·harmonyos
若兰幽竹1 天前
【HarmonyOS 6.1 全场景实战】开篇词:打造消除“吃饭焦虑”的《灵犀厨房》
harmonyos·鸿蒙开发·华为鸿蒙系统
机构师1 天前
<鸿蒙><APP><3D>鸿蒙3D开发,如何获取ktx格式的天空盒图?
华为·harmonyos