文章目录
-
- 每日一句正能量
- 前言
- 一、前言:科学教育的交互革新需求
- 二、核心特性解析与技术选型
-
- [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 真机调试建议
- 双AR引擎性能:同时运行Face AR和Body AR对GPU要求较高,建议使用高性能PC真机
- 手势识别校准:不同实验场景需要调整手势识别阈值,建议提供校准界面
- 安全警示测试:验证危险状态时的红色闪烁光效是否足够醒目
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端的"量子实验室"沉浸式科学实验与虚拟仿真平台。核心创新点总结:
-
学科感知光效+安全警示系统:根据实验学科动态切换主题色,安全等级实时反馈警示光效,营造真实实验室的安全氛围
-
Face AR + Body AR双引擎融合:Face AR识别表情意图(精细/快速模式),Body AR执行具体操作(抓取/倾倒/搅拌),实现"意图+动作"的自然交互
-
悬浮实验导航:底部悬浮页签替代传统实验菜单,玻璃拟态设计+三档透明度调节,在保持导航可达性的同时最大化实验区域
-
PC级多窗口科研 :主实验窗口 + 浮动数据记录 + 仪器面板 + 三维分子视图的四层架构,通过
WindowManager实现跨窗口光效联动与焦点感知
未来扩展方向:
- 接入分布式软总线,实现跨设备协同实验(手机AR观察、平板数据记录、PC主控仿真)
- AI实验助手:基于当前实验状态,AI推荐下一步操作并预警潜在危险
- VR/AR融合实验:支持VR头显接入,实现完全沉浸式的分子级微观实验
- 远程实验协作:多人实时在线协作实验,通过AR同步操作意图
转载自:https://blog.csdn.net/u014727709/article/details/138339290
欢迎 👍点赞✍评论⭐收藏,欢迎指正