HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与Face AR & Body AR的“灵犀康养“——PC端沉浸式AR智能康复训练系统
摘要:HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR & Body AR特性,为医疗康复领域开辟了全新的交互维度。本文将实战开发一款面向HarmonyOS PC的"灵犀康养"智能康复系统,展示如何利用构建随康复阶段动态变化的治疗环境光感,通过悬浮导航实现训练项目与康复阶段的快速切换,基于Face AR实现患者情绪状态与疼痛反馈的实时捕捉,基于Body AR实现康复动

每日一句正能量
给自己一个清晰的目标,恰恰是走出犹豫,开始行动的关键。
犹豫的根源 往往不是懒,而是目标模糊。不知道要什么,自然无法决定第一步。清晰的目标 提供方向感、筛选标准和动力。哪怕目标很小(比如“今天读10页书”),也能立刻打破“做还是不做”的内耗。不是等不犹豫了才定目标,而是定了目标才能不再犹豫。
前言
摘要:HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR & Body AR特性,为医疗康复领域开辟了全新的交互维度。本文将实战开发一款面向HarmonyOS PC的"灵犀康养"智能康复系统,展示如何利用
systemMaterialEffect构建随康复阶段动态变化的治疗环境光感,通过悬浮导航实现训练项目与康复阶段的快速切换,基于Face AR实现患者情绪状态与疼痛反馈的实时捕捉,基于Body AR实现康复动作的精准追踪与关节角度分析,以及基于多窗口架构构建训练指导、康复数据、医生反馈和家属关怀的协作康复界面。
一、前言:康复训练的交互范式革新
传统康复训练往往依赖治疗师一对一指导和固定器械,患者难以获得实时反馈,训练数据也难以量化追踪。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation)、沉浸光感(Immersive Light Effects)与Face AR & Body AR特性,为康复训练带来了"情绪即反馈、肢体即处方"的全新可能。
本文核心亮点:
- 阶段感知光效:根据康复阶段(急性期/恢复期/强化期/维持期)动态切换治疗环境光色与氛围
- 情绪疼痛监测:通过Face AR实时捕捉患者微表情,识别疼痛、焦虑、疲惫等状态,自动调整训练强度
- 动作精准追踪:Body AR追踪20+骨骼关键点,实时分析关节角度、活动范围、对称性
- 悬浮项目导航:底部悬浮页签切换训练项目,支持透明度调节,最大化训练画面区域
- 多窗口康复协作:主训练窗口 + 浮动动作指导 + 浮动数据面板 + 浮动医生视频 + 浮动家属关怀
二、核心特性解析与技术选型
2.1 沉浸光感在康复场景中的价值
HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为UI组件带来细腻的光晕与反射效果。在康复场景中,这种材质效果能够:
- 增强治疗氛围:急性期的冷静蓝光缓解焦虑、恢复期的温暖绿光促进愈合、强化期的活力橙光激发动力
- 进度可视化:康复进度通过光效强度变化直观呈现,增强患者信心
- 情绪调节:检测到焦虑情绪时自动切换为柔和光效,降低患者紧张感
- 安全警示:动作超出安全范围时标题栏泛红警示,防止二次损伤
2.2 Face AR在康复中的创新应用
HarmonyOS 6的Face AR能力支持实时精确捕捉人脸微表情(64种BlendShape参数),在康复中可以:
- 疼痛等级评估:通过皱眉、咬牙、眼部眯起等微表情自动评估疼痛等级(0-10分)
- 情绪状态识别:识别焦虑、沮丧、积极等情绪,辅助心理治疗师调整方案
- 疲劳度监测:通过眨眼频率、打哈欠、眼神游离判断疲劳程度,建议休息
- 康复依从性:记录训练过程中的表情变化,生成"康复情绪档案"供医生参考
2.3 Body AR在动作追踪中的创新应用
HarmonyOS 6的Body AR能力支持20+骨骼关键点追踪,在康复动作追踪中可以:
- 关节角度测量:实时测量肩、肘、腕、髋、膝、踝等关节的活动角度
- 动作对称性分析:对比左右侧肢体动作差异,评估康复效果
- 运动轨迹记录:记录肢体运动轨迹,与标准动作比对,给出纠正建议
- 跌倒风险预警:检测身体重心偏移、步态异常,及时预警跌倒风险
三、环境配置与权限声明
3.1 模块依赖配置
在oh-package.json5中添加AR Engine、图形引擎和UI Design Kit依赖:
{
"dependencies": {
"@hms.core.ar.engine": "^6.1.0",
"@hms.core.graphics.2d": "^6.0.0",
"@hms.core.arkui.design": "^6.0.0",
"@hms.core.health.kit": "^6.0.0",
"@hms.core.ai.vision": "^6.0.0",
"@hms.core.distributed.device": "^6.0.0"
}
}
3.2 权限声明(module.json5)
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:body_ar_camera_permission",
"usedScene": {
"abilities": ["RehabAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_permission"
},
{
"name": "ohos.permission.HEALTH_DATA",
"reason": "$string:health_data_access"
},
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "$string:doctor_sync"
}
]
}
}
隐私说明:Face AR与Body AR的所有图像数据仅在端侧NPU处理,患者生物特征数据不上传云端,符合医疗隐私保护要求。
四、核心代码实战
4.1 康复阶段光感引擎(RehabLightEngine.ets)
代码亮点:根据康复阶段动态生成治疗环境光感参数,支持光效过渡、情绪调节和安全警示。
// engines/RehabLightEngine.ets
import { ColorUtils } from '@hms.core.graphics.2d';
export enum RehabPhase {
ACUTE = 'acute', // 急性期 - 冷静蓝光,缓解疼痛焦虑
RECOVERY = 'recovery', // 恢复期 - 温暖绿光,促进愈合
STRENGTHEN = 'strengthen', // 强化期 - 活力橙光,激发动力
MAINTAIN = 'maintain' // 维持期 - 稳定白光,保持状态
}
export enum RehabMood {
CALM = 'calm', // 平静
ANXIOUS = 'anxious', // 焦虑
PAINFUL = 'painful', // 疼痛
TIRED = 'tired', // 疲劳
MOTIVATED = 'motivated', // 积极
FRUSTRATED = 'frustrated' // 挫败
}
export interface RehabLightTheme {
primaryColor: ResourceColor;
secondaryColor: ResourceColor;
ambientColor: ResourceColor;
safeColor: ResourceColor; // 安全范围指示色
warningColor: ResourceColor; // 警告色
glowIntensity: number;
pulseSpeed: number;
colorTemperature: number;
brightness: number;
relaxationMode: boolean; // 放松模式
}
export class RehabLightEngine {
private static themes: Map<RehabPhase, RehabLightTheme> = new Map([
[RehabPhase.ACUTE, {
primaryColor: '#3B82F6', // 冷静蓝
secondaryColor: '#1E40AF', // 深蓝
ambientColor: 'rgba(59, 130, 246, 0.08)',
safeColor: '#4ADE80', // 安全绿
warningColor: '#EF4444', // 警告红
glowIntensity: 0.4,
pulseSpeed: 6000,
colorTemperature: 5500,
brightness: 0.75,
relaxationMode: true
}],
[RehabPhase.RECOVERY, {
primaryColor: '#22C55E', // 愈合绿
secondaryColor: '#15803D', // 深绿
ambientColor: 'rgba(34, 197, 94, 0.1)',
safeColor: '#4ADE80',
warningColor: '#F59E0B',
glowIntensity: 0.6,
pulseSpeed: 4000,
colorTemperature: 4200,
brightness: 0.85,
relaxationMode: true
}],
[RehabPhase.STRENGTHEN, {
primaryColor: '#F97316', // 活力橙
secondaryColor: '#C2410C', // 深橙
ambientColor: 'rgba(249, 115, 22, 0.12)',
safeColor: '#4ADE80',
warningColor: '#EF4444',
glowIntensity: 0.8,
pulseSpeed: 2500,
colorTemperature: 3800,
brightness: 0.9,
relaxationMode: false
}],
[RehabPhase.MAINTAIN, {
primaryColor: '#E2E8F0', // 稳定白
secondaryColor: '#94A3B8', // 灰
ambientColor: 'rgba(226, 232, 240, 0.06)',
safeColor: '#4ADE80',
warningColor: '#F59E0B',
glowIntensity: 0.5,
pulseSpeed: 5000,
colorTemperature: 4500,
brightness: 0.85,
relaxationMode: true
}]
]);
static getTheme(phase: RehabPhase): RehabLightTheme {
return this.themes.get(phase) || this.themes.get(RehabPhase.ACUTE)!;
}
// 根据患者情绪调整光效
static adjustByMood(theme: RehabLightTheme, mood: RehabMood): RehabLightTheme {
const adjusted = { ...theme };
switch (mood) {
case RehabMood.ANXIOUS:
adjusted.primaryColor = '#818CF8'; // 柔和紫蓝安抚
adjusted.glowIntensity *= 0.7;
adjusted.pulseSpeed *= 1.5;
adjusted.relaxationMode = true;
break;
case RehabMood.PAINFUL:
adjusted.primaryColor = '#FCA5A5'; // 淡粉缓解
adjusted.glowIntensity *= 0.6;
adjusted.brightness *= 0.85;
adjusted.relaxationMode = true;
break;
case RehabMood.TIRED:
adjusted.brightness *= 0.8;
adjusted.glowIntensity *= 0.7;
adjusted.colorTemperature = Math.max(2700, adjusted.colorTemperature - 300);
break;
case RehabMood.MOTIVATED:
adjusted.glowIntensity *= 1.2;
adjusted.pulseSpeed *= 0.8;
adjusted.brightness *= 1.05;
break;
case RehabMood.FRUSTRATED:
adjusted.primaryColor = '#FBBF24'; // 暖黄鼓励
adjusted.glowIntensity *= 1.1;
break;
case RehabMood.CALM:
// 保持默认
break;
}
return adjusted;
}
// 安全警示光效
static getSafetyLight(isSafe: boolean, intensity: number = 1): RehabLightTheme {
const base = this.getTheme(RehabPhase.RECOVERY);
if (isSafe) {
return { ...base, primaryColor: base.safeColor, glowIntensity: 0.6 * intensity };
}
return {
...base,
primaryColor: base.warningColor,
glowIntensity: 1.0 * intensity,
pulseSpeed: 1000 // 快速闪烁警示
};
}
}
4.2 Face AR疼痛与情绪评估系统(PainEmotionSystem.ets)
代码亮点:利用Face AR的64种BlendShape参数,实时评估患者疼痛等级与情绪状态,自动调整康复方案。
// systems/PainEmotionSystem.ets
import { ARSession, ARFaceTrack, ARBlendShapes } from '@hms.core.ar.engine';
export interface PainAssessment {
level: number; // 疼痛等级 0-10
type: 'sharp' | 'dull' | 'burning' | 'aching' | 'throbbing';
confidence: number;
triggers: string[]; // 触发动作
}
export interface EmotionState {
primary: RehabMood;
intensity: number; // 情绪强度 0-1
stability: number; // 稳定性 0-1
trend: 'improving' | 'stable' | 'worsening';
}
export class PainEmotionSystem {
private session: ARSession | null = null;
private faceTrack: ARFaceTrack | null = null;
// 历史记录
private expressionHistory: FaceExpression[] = [];
private painHistory: PainAssessment[] = [];
private emotionHistory: EmotionState[] = [];
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.FACE],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_720P
}
});
this.faceTrack = this.session.getFaceTrack();
await this.session.start();
}
// 每帧更新:评估疼痛与情绪
update(frameData: ARFrame): { pain: PainAssessment; emotion: EmotionState } | null {
if (!this.faceTrack) return null;
const faces = this.faceTrack.getTrackedFaces(frameData);
if (faces.length === 0) return null;
const face = faces[0];
const blendshapes = face.getBlendShapes();
const expression = this.parseExpression(blendshapes);
this.expressionHistory.push(expression);
if (this.expressionHistory.length > 30) this.expressionHistory.shift();
const pain = this.assessPain(expression);
const emotion = this.assessEmotion(expression);
this.painHistory.push(pain);
this.emotionHistory.push(emotion);
// 通知系统调整
AppStorage.set('current_pain_level', pain.level);
AppStorage.set('current_emotion_state', emotion);
return { pain, emotion };
}
private parseExpression(blendshapes: ARBlendShapes): FaceExpression {
return {
browLower: blendshapes.getValue('BROW_LOWERER') || 0,
browRaise: blendshapes.getValue('BROW_RAISE') || 0,
eyeSquintLeft: blendshapes.getValue('EYE_SQUINT_LEFT') || 0,
eyeSquintRight: blendshapes.getValue('EYE_SQUINT_RIGHT') || 0,
noseWrinkle: blendshapes.getValue('NOSE_WRINKLE') || 0,
mouthOpen: blendshapes.getValue('MOUTH_OPEN') || 0,
jawClench: blendshapes.getValue('JAW_CLENCH') || 0,
lipCornerDepress: blendshapes.getValue('MOUTH_CORNER_DEPRESS_LEFT') || 0,
eyeBlink: blendshapes.getValue('EYE_BLINK_LEFT') || 0,
timestamp: Date.now()
};
}
private assessPain(expression: FaceExpression): PainAssessment {
let level = 0;
const triggers: string[] = [];
// 皱眉程度
if (expression.browLower > 0.3) {
level += expression.browLower * 3;
triggers.push('皱眉');
}
// 眼部眯起(疼痛典型反应)
const eyeSquint = (expression.eyeSquintLeft + expression.eyeSquintRight) / 2;
if (eyeSquint > 0.4) {
level += eyeSquint * 2.5;
triggers.push('眯眼');
}
// 鼻子皱起
if (expression.noseWrinkle > 0.3) {
level += expression.noseWrinkle * 2;
triggers.push('鼻皱');
}
// 咬牙/张嘴
if (expression.jawClench > 0.5) {
level += expression.jawClench * 1.5;
triggers.push('咬牙');
}
// 嘴角下垂(痛苦表情)
if (expression.lipCornerDepress > 0.4) {
level += expression.lipCornerDepress * 1.5;
triggers.push('嘴角下垂');
}
level = Math.min(10, Math.max(0, level));
// 判断疼痛类型
let type: PainAssessment['type'] = 'aching';
if (expression.jawClench > 0.6 && expression.browLower > 0.6) {
type = 'sharp';
} else if (expression.eyeSquintLeft > 0.7 && expression.eyeSquintRight > 0.7) {
type = 'burning';
} else if (expression.mouthOpen > 0.5 && expression.browLower < 0.4) {
type = 'dull';
}
return {
level: Math.round(level * 10) / 10,
type,
confidence: Math.min(1, level / 5),
triggers
};
}
private assessEmotion(expression: FaceExpression): EmotionState {
// 分析最近10帧的趋势
const recent = this.expressionHistory.slice(-10);
let anxiousScore = 0;
let tiredScore = 0;
let motivatedScore = 0;
let frustratedScore = 0;
recent.forEach(expr => {
// 焦虑:频繁眨眼 + 眉毛抬高
if (expr.eyeBlink > 0.6 && expr.browRaise > 0.3) anxiousScore++;
// 疲劳:眨眼频率低 + 眼神呆滞
if (expr.eyeBlink < 0.2 && expr.browLower < 0.2) tiredScore++;
// 积极:微笑 + 眼神明亮
if (expr.browRaise > 0.2 && expr.browLower < 0.3) motivatedScore++;
// 挫败:紧咬牙关 + 嘴角下垂
if (expr.jawClench > 0.4 && expr.lipCornerDepress > 0.3) frustratedScore++;
});
const scores = [
{ mood: RehabMood.ANXIOUS, score: anxiousScore },
{ mood: RehabMood.TIRED, score: tiredScore },
{ mood: RehabMood.MOTIVATED, score: motivatedScore },
{ mood: RehabMood.FRUSTRATED, score: frustratedScore }
];
scores.sort((a, b) => b.score - a.score);
const primary = scores[0].score > 3 ? scores[0].mood : RehabMood.CALM;
// 计算稳定性
const stability = this.expressionHistory.length > 10
? 1 - (this.calculateVariance(recent.map(e => e.browLower)) * 2)
: 0.5;
// 判断趋势
let trend: EmotionState['trend'] = 'stable';
if (this.emotionHistory.length > 5) {
const previous = this.emotionHistory[this.emotionHistory.length - 5];
if (scores[0].score > 3 && previous.primary !== primary) {
trend = scores[0].score > previous.intensity * 10 ? 'worsening' : 'improving';
}
}
return {
primary,
intensity: scores[0].score / 10,
stability: Math.max(0, Math.min(1, stability)),
trend
};
}
private calculateVariance(values: number[]): number {
const mean = values.reduce((a, b) => a + b, 0) / values.length;
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
return Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / values.length);
}
// 获取疼痛历史
getPainHistory(): PainAssessment[] {
return [...this.painHistory];
}
// 获取情绪历史
getEmotionHistory(): EmotionState[] {
return [...this.emotionHistory];
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
interface FaceExpression {
browLower: number;
browRaise: number;
eyeSquintLeft: number;
eyeSquintRight: number;
noseWrinkle: number;
mouthOpen: number;
jawClench: number;
lipCornerDepress: number;
eyeBlink: number;
timestamp: number;
}
4.3 Body AR康复动作追踪系统(RehabMotionSystem.ets)
代码亮点:将Body AR的20+骨骼关键点映射为康复动作分析,实现精准的动作评估与纠正。
// systems/RehabMotionSystem.ets
import { ARSession, ARBodyTrack, ARBodySkeleton, KeyPointType } from '@hms.core.ar.engine';
export enum JointType {
SHOULDER_LEFT = 'shoulder_left',
SHOULDER_RIGHT = 'shoulder_right',
ELBOW_LEFT = 'elbow_left',
ELBOW_RIGHT = 'elbow_right',
WRIST_LEFT = 'wrist_left',
WRIST_RIGHT = 'wrist_right',
HIP_LEFT = 'hip_left',
HIP_RIGHT = 'hip_right',
KNEE_LEFT = 'knee_left',
KNEE_RIGHT = 'knee_right',
ANKLE_LEFT = 'ankle_left',
ANKLE_RIGHT = 'ankle_right'
}
export interface JointAngle {
joint: JointType;
angle: number; // 角度 0-180
targetRange: { min: number; max: number };
deviation: number; // 偏离度
isSafe: boolean;
}
export interface MotionAnalysis {
timestamp: number;
jointAngles: JointAngle[];
symmetry: number; // 对称性 0-100
stability: number; // 稳定性 0-100
rangeOfMotion: number; // 活动度 0-100
repetitions: number; // 重复次数
formScore: number; // 动作质量 0-100
feedback: string[];
}
export interface RehabExercise {
id: string;
name: string;
targetJoints: JointType[];
targetRange: { min: number; max: number };
repetitions: number;
holdTime: number; // 保持时间(秒)
restTime: number; // 休息时间(秒)
}
export class RehabMotionSystem {
private session: ARSession | null = null;
private bodyTrack: ARBodyTrack | null = null;
// 运动历史
private motionHistory: MotionAnalysis[] = [];
private currentExercise: RehabExercise | null = null;
private repCount: number = 0;
private lastRepTime: number = 0;
private inHoldPhase: boolean = false;
private holdStartTime: number = 0;
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.BODY],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_720P
}
});
this.bodyTrack = this.session.getBodyTrack();
await this.session.start();
}
// 每帧更新:分析康复动作
update(frameData: ARFrame, exercise: RehabExercise): MotionAnalysis | null {
if (!this.bodyTrack) return null;
const bodies = this.bodyTrack.getTrackedBodies(frameData);
if (bodies.length === 0) return null;
const body = bodies[0];
const skeleton = body.getSkeleton();
const keypoints = skeleton.getKeyPoints();
this.currentExercise = exercise;
// 计算各关节角度
const jointAngles = this.calculateJointAngles(keypoints, exercise);
// 计算对称性
const symmetry = this.calculateSymmetry(jointAngles);
// 计算稳定性
const stability = this.calculateStability(jointAngles);
// 计算活动度
const rangeOfMotion = this.calculateRangeOfMotion(jointAngles, exercise);
// 计数重复次数
const repetitions = this.countRepetitions(jointAngles, exercise);
// 评估动作质量
const { formScore, feedback } = this.assessForm(jointAngles, exercise);
const analysis: MotionAnalysis = {
timestamp: Date.now(),
jointAngles,
symmetry,
stability,
rangeOfMotion,
repetitions: this.repCount,
formScore,
feedback
};
this.motionHistory.push(analysis);
if (this.motionHistory.length > 120) this.motionHistory.shift();
// 通知UI更新
AppStorage.set('motion_analysis', analysis);
return analysis;
}
private calculateJointAngles(keypoints: KeyPoint[], exercise: RehabExercise): JointAngle[] {
const angles: JointAngle[] = [];
exercise.targetJoints.forEach(joint => {
const angle = this.measureJointAngle(keypoints, joint);
const deviation = Math.abs(angle - (exercise.targetRange.min + exercise.targetRange.max) / 2);
const isSafe = angle >= exercise.targetRange.min - 10 && angle <= exercise.targetRange.max + 10;
angles.push({
joint,
angle,
targetRange: exercise.targetRange,
deviation,
isSafe
});
});
return angles;
}
private measureJointAngle(keypoints: KeyPoint[], joint: JointType): number {
// 根据关节类型选择对应的骨骼点
let p1: KeyPoint | undefined, p2: KeyPoint | undefined, p3: KeyPoint | undefined;
switch (joint) {
case JointType.ELBOW_LEFT:
p1 = keypoints.find(kp => kp.type === KeyPointType.LEFT_SHOULDER);
p2 = keypoints.find(kp => kp.type === KeyPointType.LEFT_ELBOW);
p3 = keypoints.find(kp => kp.type === KeyPointType.LEFT_WRIST);
break;
case JointType.ELBOW_RIGHT:
p1 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_SHOULDER);
p2 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ELBOW);
p3 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_WRIST);
break;
case JointType.KNEE_LEFT:
p1 = keypoints.find(kp => kp.type === KeyPointType.LEFT_HIP);
p2 = keypoints.find(kp => kp.type === KeyPointType.LEFT_KNEE);
p3 = keypoints.find(kp => kp.type === KeyPointType.LEFT_ANKLE);
break;
case JointType.KNEE_RIGHT:
p1 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_HIP);
p2 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_KNEE);
p3 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ANKLE);
break;
case JointType.SHOULDER_LEFT:
p1 = keypoints.find(kp => kp.type === KeyPointType.LEFT_ELBOW);
p2 = keypoints.find(kp => kp.type === KeyPointType.LEFT_SHOULDER);
p3 = keypoints.find(kp => kp.type === KeyPointType.LEFT_HIP);
break;
case JointType.SHOULDER_RIGHT:
p1 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ELBOW);
p2 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_SHOULDER);
p3 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_HIP);
break;
}
if (!p1 || !p2 || !p3) return 0;
// 计算向量夹角
const v1 = { x: p1.x - p2.x, y: p1.y - p2.y };
const v2 = { x: p3.x - p2.x, y: p3.y - p2.y };
const dot = v1.x * v2.x + v1.y * v2.y;
const mag1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
const mag2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
if (mag1 === 0 || mag2 === 0) return 0;
const cosAngle = dot / (mag1 * mag2);
return Math.acos(Math.max(-1, Math.min(1, cosAngle))) * (180 / Math.PI);
}
private calculateSymmetry(jointAngles: JointAngle[]): number {
// 计算左右对称性
const leftJoints = jointAngles.filter(j => j.joint.includes('left'));
const rightJoints = jointAngles.filter(j => j.joint.includes('right'));
if (leftJoints.length === 0 || rightJoints.length === 0) return 100;
let totalDiff = 0;
leftJoints.forEach(left => {
const right = rightJoints.find(r =>
r.joint.replace('right', 'left') === left.joint
);
if (right) {
totalDiff += Math.abs(left.angle - right.angle);
}
});
return Math.max(0, 100 - (totalDiff / leftJoints.length) * 2);
}
private calculateStability(jointAngles: JointAngle[]): number {
if (this.motionHistory.length < 5) return 100;
const recent = this.motionHistory.slice(-5);
let variance = 0;
jointAngles.forEach(joint => {
const angles = recent.map(m => {
const j = m.jointAngles.find(ja => ja.joint === joint.joint);
return j ? j.angle : 0;
});
variance += this.calculateVariance(angles);
});
return Math.max(0, 100 - variance * 5);
}
private calculateRangeOfMotion(jointAngles: JointAngle[], exercise: RehabExercise): number {
const targetMid = (exercise.targetRange.min + exercise.targetRange.max) / 2;
const currentMid = jointAngles.reduce((sum, j) => sum + j.angle, 0) / jointAngles.length;
return Math.min(100, (currentMid / targetMid) * 100);
}
private countRepetitions(jointAngles: JointAngle[], exercise: RehabExercise): number {
const now = Date.now();
const avgAngle = jointAngles.reduce((sum, j) => sum + j.angle, 0) / jointAngles.length;
// 检测动作峰值
const isAtPeak = avgAngle >= exercise.targetRange.max * 0.9;
const isAtRest = avgAngle <= exercise.targetRange.min * 1.1;
if (isAtPeak && !this.inHoldPhase) {
this.inHoldPhase = true;
this.holdStartTime = now;
}
if (isAtRest && this.inHoldPhase) {
const holdDuration = (now - this.holdStartTime) / 1000;
if (holdDuration >= exercise.holdTime * 0.8) { // 允许20%容差
this.repCount++;
this.lastRepTime = now;
this.inHoldPhase = false;
// 播放完成音效
AppStorage.set('rep_completed', this.repCount);
}
}
return this.repCount;
}
private assessForm(jointAngles: JointAngle[], exercise: RehabExercise): { formScore: number; feedback: string[] } {
const feedback: string[] = [];
let score = 100;
// 检查各关节是否在安全范围
jointAngles.forEach(joint => {
if (!joint.isSafe) {
score -= 15;
feedback.push(`${this.getJointName(joint.joint)}角度超出安全范围`);
}
});
// 检查对称性
const symmetry = this.calculateSymmetry(jointAngles);
if (symmetry < 80) {
score -= 10;
feedback.push('左右动作不对称,请注意平衡');
}
// 检查稳定性
const stability = this.calculateStability(jointAngles);
if (stability < 70) {
score -= 10;
feedback.push('动作不够稳定,请控制速度');
}
// 鼓励反馈
if (score >= 90) {
feedback.push('动作标准,继续保持!');
} else if (score >= 70) {
feedback.push('动作良好,稍加调整即可');
}
return { formScore: Math.max(0, score), feedback };
}
private getJointName(joint: JointType): string {
const names: Map<JointType, string> = new Map([
[JointType.SHOULDER_LEFT, '左肩'],
[JointType.SHOULDER_RIGHT, '右肩'],
[JointType.ELBOW_LEFT, '左肘'],
[JointType.ELBOW_RIGHT, '右肘'],
[JointType.WRIST_LEFT, '左腕'],
[JointType.WRIST_RIGHT, '右腕'],
[JointType.HIP_LEFT, '左髋'],
[JointType.HIP_RIGHT, '右髋'],
[JointType.KNEE_LEFT, '左膝'],
[JointType.KNEE_RIGHT, '右膝'],
[JointType.ANKLE_LEFT, '左踝'],
[JointType.ANKLE_RIGHT, '右踝']
]);
return names.get(joint) || joint;
}
private calculateVariance(values: number[]): number {
const mean = values.reduce((a, b) => a + b, 0) / values.length;
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
return Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / values.length);
}
// 获取运动历史
getMotionHistory(): MotionAnalysis[] {
return [...this.motionHistory];
}
// 重置计数
resetCount(): void {
this.repCount = 0;
this.inHoldPhase = false;
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
4.4 沉浸光感康复标题栏(ImmersiveRehabTitleBar.ets)
代码亮点:标题栏随康复阶段和患者情绪动态变化,显示训练进度、疼痛等级和情绪状态。
// components/ImmersiveRehabTitleBar.ets
import { RehabLightEngine, RehabPhase, RehabMood } from '../engines/RehabLightEngine';
import { PainAssessment } from '../systems/PainEmotionSystem';
import { MotionAnalysis } from '../systems/RehabMotionSystem';
@Component
export struct ImmersiveRehabTitleBar {
@Prop currentPhase: RehabPhase;
@Prop patientName: string;
@Prop exerciseName: string;
@Prop painAssessment: PainAssessment;
@Prop motionAnalysis: MotionAnalysis;
@Prop rehabProgress: number; // 0-100
@State theme = RehabLightEngine.getTheme(RehabPhase.ACUTE);
@State pulseAnimation: boolean = false;
aboutToAppear(): void {
this.theme = RehabLightEngine.getTheme(this.currentPhase);
setInterval(() => {
this.pulseAnimation = !this.pulseAnimation;
}, this.theme.pulseSpeed / 2);
}
build() {
Row() {
// 左侧:患者与阶段信息
Row({ space: 12 }) {
// 康复阶段徽标
Stack() {
Circle()
.width(44)
.height(44)
.fill(this.theme.primaryColor)
.shadow({
radius: this.pulseAnimation ? 20 : 8,
color: this.theme.primaryColor,
offsetX: 0,
offsetY: 0
})
.animation({
duration: this.theme.pulseSpeed / 2,
curve: Curve.EaseInOut,
iterations: -1
})
Text(this.getPhaseIcon())
.fontSize(22)
}
Column({ space: 4 }) {
Text(this.patientName)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(`${this.getPhaseName()} | ${this.exerciseName}`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.7)')
}
}
// 中间:疼痛与动作数据
Row({ space: 20 }) {
// 疼痛等级
Column({ space: 2 }) {
Row({ space: 4 }) {
Text('😣')
.fontSize(12)
Text(`疼痛: ${this.painAssessment.level.toFixed(1)}/10`)
.fontSize(14)
.fontColor(this.getPainColor())
}
Progress({ value: this.painAssessment.level, total: 10, type: ProgressType.Linear })
.width(80)
.height(4)
.color(this.getPainColor())
.backgroundColor('rgba(255,255,255,0.2)')
}
// 动作质量
Column({ space: 2 }) {
Row({ space: 4 }) {
Text('⭐')
.fontSize(12)
Text(`${this.motionAnalysis.formScore.toFixed(0)}分`)
.fontSize(14)
.fontColor(this.motionAnalysis.formScore > 80 ? '#4ADE80' : '#FBBF24')
}
Progress({ value: this.motionAnalysis.formScore, total: 100, type: ProgressType.Linear })
.width(80)
.height(4)
.color(this.motionAnalysis.formScore > 80 ? '#4ADE80' : '#FBBF24')
.backgroundColor('rgba(255,255,255,0.2)')
}
// 重复次数
Column({ space: 2 }) {
Text('🔁')
.fontSize(12)
Text(`${this.motionAnalysis.repetitions}次`)
.fontSize(14)
.fontColor('#FFFFFF')
}
// 对称性
Column({ space: 2 }) {
Text('⚖️')
.fontSize(12)
Text(`${this.motionAnalysis.symmetry.toFixed(0)}%`)
.fontSize(14)
.fontColor(this.motionAnalysis.symmetry > 80 ? '#4ADE80' : '#F59E0B')
}
}
// 右侧:康复进度
Row({ space: 12 }) {
// 安全状态
if (!this.motionAnalysis.jointAngles.every(j => j.isSafe)) {
Row({ space: 4 }) {
Text('⚠️')
.fontSize(14)
Text('注意安全')
.fontSize(11)
.fontColor('#EF4444')
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('rgba(239, 68, 68, 0.15)')
.borderRadius(12)
.animation({
duration: 800,
curve: Curve.EaseInOut,
iterations: -1
})
}
// 总体进度
Column({ space: 2 }) {
Text('📈')
.fontSize(12)
Text(`${this.rehabProgress.toFixed(0)}%`)
.fontSize(14)
.fontColor('#FFFFFF')
}
}
}
.width('100%')
.height(64)
.padding({ left: 24, right: 24 })
.backgroundColor(this.theme.ambientColor)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.borderRadius({ bottomLeft: 20, bottomRight: 20 })
.shadow({
radius: 25,
color: this.theme.primaryColor,
offsetX: 0,
offsetY: 6
})
.justifyContent(FlexAlign.SpaceBetween)
}
private getPhaseIcon(): string {
const icons: Map<RehabPhase, string> = new Map([
[RehabPhase.ACUTE, '🧊'],
[RehabPhase.RECOVERY, '🌱'],
[RehabPhase.STRENGTHEN, '💪'],
[RehabPhase.MAINTAIN, '✅']
]);
return icons.get(this.currentPhase) || '❓';
}
private getPhaseName(): string {
const names: Map<RehabPhase, string> = new Map([
[RehabPhase.ACUTE, '急性期'],
[RehabPhase.RECOVERY, '恢复期'],
[RehabPhase.STRENGTHEN, '强化期'],
[RehabPhase.MAINTAIN, '维持期']
]);
return names.get(this.currentPhase) || '未知';
}
private getPainColor(): ResourceColor {
const level = this.painAssessment.level;
if (level <= 3) return '#4ADE80';
if (level <= 6) return '#FBBF24';
return '#EF4444';
}
}
4.5 悬浮训练导航面板(FloatExerciseNav.ets)
代码亮点:底部悬浮面板采用HdsTabs悬浮样式,四周留白,支持训练项目切换和透明度调节。
// components/FloatExerciseNav.ets
import { HdsTabs, HdsTabBarStyle } from '@hms.core.arkui.design';
import { RehabLightEngine, RehabPhase } from '../engines/RehabLightEngine';
interface ExerciseItem {
title: string;
icon: Resource;
phase: RehabPhase;
exercise: string;
targetJoints: string[];
}
@Component
export struct FloatExerciseNav {
@Prop currentPhase: RehabPhase;
@Prop currentExercise: string;
@Prop transparencyLevel: number;
@State selectedIndex: number = 0;
@State theme = RehabLightEngine.getTheme(RehabPhase.ACUTE);
private exercises: ExerciseItem[] = [
{ title: '肩外展', icon: $r('app.media.exer_shoulder'), phase: RehabPhase.ACUTE, exercise: 'shoulder_abduction', targetJoints: ['肩关节'] },
{ title: '肘屈伸', icon: $r('app.media.exer_elbow'), phase: RehabPhase.RECOVERY, exercise: 'elbow_flexion', targetJoints: ['肘关节'] },
{ title: '膝屈伸', icon: $r('app.media.exer_knee'), phase: RehabPhase.RECOVERY, exercise: 'knee_flexion', targetJoints: ['膝关节'] },
{ title: '踝泵', icon: $r('app.media.exer_ankle'), phase: RehabPhase.ACUTE, exercise: 'ankle_pump', targetJoints: ['踝关节'] },
{ title: '平衡', icon: $r('app.media.exer_balance'), phase: RehabPhase.STRENGTHEN, exercise: 'balance', targetJoints: ['髋关节', '膝关节', '踝关节'] },
{ title: '步态', icon: $r('app.media.exer_gait'), phase: RehabPhase.STRENGTHEN, exercise: 'gait', targetJoints: ['全下肢'] }
];
build() {
Column() {
HdsTabs({
barStyle: HdsTabBarStyle.FLOATING,
index: this.selectedIndex,
onChange: (index: number) => {
this.selectedIndex = index;
this.handleExerciseChange(this.exercises[index]);
}
}) {
ForEach(this.exercises, (item: ExerciseItem, index: number) => {
TabContent() {
Stack() {}
}
.tabBar(this.buildTabBar(item, index))
})
}
.barBackgroundColor(`rgba(15, 15, 35, ${this.transparencyLevel})`)
.barActiveColor(this.theme.primaryColor)
.barInactiveColor('#666666')
.barHeight(72)
.barMargin({ left: 48, right: 48, bottom: 20 })
.barBorderRadius(36)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.backdropBlur(20)
}
.width('100%')
.padding({ bottom: 16 })
}
@Builder
buildTabBar(item: ExerciseItem, index: number): void {
Column({ space: 4 }) {
Stack() {
Image(item.icon)
.width(26)
.height(26)
.fillColor(index === this.selectedIndex ? this.theme.primaryColor : '#666666')
// 当前训练指示器
if (item.exercise === this.currentExercise) {
Circle()
.width(10)
.height(10)
.fill('#00F0FF')
.position({ x: 20, y: -6 })
.shadow({ radius: 6, color: 'rgba(0, 240, 255, 0.6)' })
}
}
Text(item.title)
.fontSize(11)
.fontColor(index === this.selectedIndex ? this.theme.primaryColor : '#666666')
}
.width(68)
.height(60)
.justifyContent(FlexAlign.Center)
}
private handleExerciseChange(item: ExerciseItem): void {
// 切换康复阶段光效
this.theme = RehabLightEngine.getTheme(item.phase);
AppStorage.set('switch_phase', item.phase);
// 切换训练项目
AppStorage.set('switch_exercise', item.exercise);
// 显示训练信息
AppStorage.set('exercise_info', {
name: item.title,
targetJoints: item.targetJoints,
phase: item.phase
});
}
}
4.6 主康复训练页面(RehabMainPage.ets)
代码亮点:整合AR数据流、康复光感、悬浮导航和多窗口管理,实现完整的"灵犀康养"康复体验。
// pages/RehabMainPage.ets
import { PainEmotionSystem, PainAssessment, EmotionState } from '../systems/PainEmotionSystem';
import { RehabMotionSystem, MotionAnalysis, RehabExercise, JointType } from '../systems/RehabMotionSystem';
import { RehabLightEngine, RehabPhase, RehabMood } from '../engines/RehabLightEngine';
import { ImmersiveRehabTitleBar } from '../components/ImmersiveRehabTitleBar';
import { FloatExerciseNav } from '../components/FloatExerciseNav';
@Entry
@Component
struct RehabMainPage {
// AR系统
private painSystem: PainEmotionSystem = new PainEmotionSystem();
private motionSystem: RehabMotionSystem = new RehabMotionSystem();
// 患者状态
@State patientName: string = '李先生';
@State currentPhase: RehabPhase = RehabPhase.RECOVERY;
@State currentExercise: string = 'shoulder_abduction';
@State exerciseName: string = '肩关节外展训练';
// 评估数据
@State painAssessment: PainAssessment = {
level: 2,
type: 'aching',
confidence: 0.8,
triggers: []
};
@State emotionState: EmotionState = {
primary: RehabMood.CALM,
intensity: 0.3,
stability: 0.8,
trend: 'stable'
};
@State motionAnalysis: MotionAnalysis = {
timestamp: Date.now(),
jointAngles: [],
symmetry: 85,
stability: 90,
rangeOfMotion: 75,
repetitions: 0,
formScore: 82,
feedback: ['准备开始训练']
};
@State rehabProgress: number = 45;
// 多窗口
@State showGuide: boolean = true;
@State showData: boolean = false;
@State showDoctor: boolean = false;
@State showFamily: boolean = false;
// 当前训练
private currentExerciseConfig: RehabExercise = {
id: 'shoulder_abduction',
name: '肩关节外展',
targetJoints: [JointType.SHOULDER_LEFT],
targetRange: { min: 0, max: 90 },
repetitions: 10,
holdTime: 3,
restTime: 5
};
aboutToAppear(): void {
this.setupImmersiveWindow();
this.initializeRehabSystems();
this.setupEventListeners();
}
private setupImmersiveWindow(): void {
const window = windowStage.getMainWindowSync();
window.setWindowLayoutFullScreen(true);
window.setWindowBackgroundColor('#0A0A0F');
}
private async initializeRehabSystems(): Promise<void> {
try {
await this.painSystem.initialize();
await this.motionSystem.initialize();
this.startRehabLoop();
} catch (err) {
console.error('康复系统初始化失败:', err);
}
}
private async startRehabLoop(): Promise<void> {
const loop = async () => {
try {
const frame = await this.painSystem.session?.getCurrentFrame();
if (!frame) {
requestAnimationFrame(loop);
return;
}
// Face AR:疼痛与情绪评估
const painEmotion = this.painSystem.update(frame);
if (painEmotion) {
this.painAssessment = painEmotion.pain;
this.emotionState = painEmotion.emotion;
// 根据疼痛和情绪调整光效
this.updateRehabLighting();
}
// Body AR:动作追踪
const motion = this.motionSystem.update(frame, this.currentExerciseConfig);
if (motion) {
this.motionAnalysis = motion;
// 更新康复进度
this.rehabProgress = Math.min(100, (motion.repetitions / this.currentExerciseConfig.repetitions) * 100);
}
} catch (err) {
console.error('康复循环错误:', err);
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
private updateRehabLighting(): void {
const baseTheme = RehabLightEngine.getTheme(this.currentPhase);
// 根据疼痛等级调整
if (this.painAssessment.level > 6) {
// 高疼痛:切换为安抚模式
const soothingTheme = RehabLightEngine.adjustByMood(baseTheme, RehabMood.PAINFUL);
AppStorage.set('current_rehab_theme', soothingTheme);
return;
}
// 根据情绪调整
const adjustedTheme = RehabLightEngine.adjustByMood(baseTheme, this.emotionState.primary);
// 检查动作安全性
const isSafe = this.motionAnalysis.jointAngles.every(j => j.isSafe);
if (!isSafe) {
const warningTheme = RehabLightEngine.getSafetyLight(false);
AppStorage.set('current_rehab_theme', warningTheme);
return;
}
AppStorage.set('current_rehab_theme', adjustedTheme);
}
private setupEventListeners(): void {
AppStorage.watch('switch_phase', (phase: RehabPhase) => {
this.currentPhase = phase;
});
AppStorage.watch('switch_exercise', (exercise: string) => {
this.currentExercise = exercise;
this.motionSystem.resetCount();
this.updateExerciseConfig(exercise);
});
AppStorage.watch('show_guide', (show: boolean) => {
this.showGuide = show;
});
AppStorage.watch('show_data', (show: boolean) => {
this.showData = show;
});
}
private updateExerciseConfig(exerciseId: string): void {
const configs: Map<string, RehabExercise> = new Map([
['shoulder_abduction', {
id: 'shoulder_abduction',
name: '肩关节外展',
targetJoints: [JointType.SHOULDER_LEFT, JointType.SHOULDER_RIGHT],
targetRange: { min: 0, max: 90 },
repetitions: 10,
holdTime: 3,
restTime: 5
}],
['elbow_flexion', {
id: 'elbow_flexion',
name: '肘关节屈伸',
targetJoints: [JointType.ELBOW_LEFT, JointType.ELBOW_RIGHT],
targetRange: { min: 0, max: 135 },
repetitions: 15,
holdTime: 2,
restTime: 3
}],
['knee_flexion', {
id: 'knee_flexion',
name: '膝关节屈伸',
targetJoints: [JointType.KNEE_LEFT, JointType.KNEE_RIGHT],
targetRange: { min: 0, max: 120 },
repetitions: 12,
holdTime: 3,
restTime: 4
}]
]);
this.currentExerciseConfig = configs.get(exerciseId) || this.currentExerciseConfig;
this.exerciseName = this.currentExerciseConfig.name;
}
build() {
Stack() {
// 背景环境光
Column()
.width('100%')
.height('100%')
.backgroundColor(RehabLightEngine.getTheme(this.currentPhase).ambientColor)
.animation({
duration: 1500,
curve: Curve.EaseInOut
})
// 主训练画面
Column() {
// AR人体骨架叠加
BodySkeletonOverlay({
motionAnalysis: this.motionAnalysis,
painLevel: this.painAssessment.level
})
.width('100%')
.height('100%')
// 训练指导动画
if (this.showGuide) {
ExerciseGuide({
exercise: this.currentExerciseConfig,
currentRep: this.motionAnalysis.repetitions,
formScore: this.motionAnalysis.formScore
})
.position({ x: '50%', y: '20%' })
.translate({ x: '-50%' })
}
}
.width('100%')
.height('100%')
// 沉浸光感标题栏
ImmersiveRehabTitleBar({
currentPhase: this.currentPhase,
patientName: this.patientName,
exerciseName: this.exerciseName,
painAssessment: this.painAssessment,
motionAnalysis: this.motionAnalysis,
rehabProgress: this.rehabProgress
})
.position({ x: 0, y: 0 })
.zIndex(100)
// 浮动数据面板
if (this.showData) {
FloatDataPanel({
motionHistory: this.motionSystem.getMotionHistory(),
painHistory: this.painSystem.getPainHistory(),
emotionHistory: this.painSystem.getEmotionHistory(),
onClose: () => {
this.showData = false;
}
})
.position({ x: '78%', y: '15%' })
.width(340)
.height('70%')
.zIndex(90)
}
// 浮动医生视频
if (this.showDoctor) {
FloatDoctorPanel({
doctorName: '王医生',
onClose: () => {
this.showDoctor = false;
}
})
.position({ x: '2%', y: '15%' })
.width(320)
.height('40%')
.zIndex(90)
}
// 底部悬浮训练导航
FloatExerciseNav({
currentPhase: this.currentPhase,
currentExercise: this.currentExercise,
transparencyLevel: 0.65
})
.position({ x: 0, y: '100%' })
.translate({ y: -88 })
.zIndex(100)
}
.width('100%')
.height('100%')
.backgroundColor('#0A0A0F')
}
aboutToDisappear(): void {
this.painSystem.release();
this.motionSystem.release();
}
}
// 人体骨架叠加组件
@Component
struct BodySkeletonOverlay {
@Prop motionAnalysis: MotionAnalysis;
@Prop painLevel: number;
build() {
Stack() {
Canvas(this.drawSkeleton)
.width('100%')
.height('100%')
.backgroundColor('transparent')
}
}
private drawSkeleton = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
context.clearRect(0, 0, w, h);
// 绘制关节点和连线(简化示意)
this.motionAnalysis.jointAngles.forEach(joint => {
const color = joint.isSafe
? (this.painLevel > 5 ? '#FBBF24' : '#4ADE80')
: '#EF4444';
// 绘制关节点
context.beginPath();
context.arc(w / 2, h / 2, 20, 0, Math.PI * 2);
context.fillStyle = color;
context.fill();
// 绘制角度指示
context.fillStyle = '#FFFFFF';
context.font = '16px sans-serif';
context.textAlign = 'center';
context.fillText(`${joint.angle.toFixed(0)}°`, w / 2, h / 2 + 40);
});
};
}
// 训练指导组件
@Component
struct ExerciseGuide {
@Prop exercise: RehabExercise;
@Prop currentRep: number;
@Prop formScore: number;
build() {
Column({ space: 16 }) {
Text(this.exercise.name)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(`目标: ${exercise.repetitions}次 | 保持${exercise.holdTime}秒`)
.fontSize(14)
.fontColor('rgba(255,255,255,0.7)')
// 进度环
Stack() {
Circle()
.width(120)
.height(120)
.stroke('rgba(255,255,255,0.2)')
.strokeWidth(8)
.fill('transparent')
Circle()
.width(120)
.height(120)
.stroke(this.currentRep >= this.exercise.repetitions ? '#4ADE80' : '#3B82F6')
.strokeWidth(8)
.fill('transparent')
.strokeDashArray([
(this.currentRep / this.exercise.repetitions) * 377,
377
])
.rotate({ angle: -90, centerX: '50%', centerY: '50%' })
Text(`${this.currentRep}/${this.exercise.repetitions}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
// 动作质量
Text(`动作质量: ${this.formScore.toFixed(0)}分`)
.fontSize(16)
.fontColor(this.formScore > 80 ? '#4ADE80' : '#FBBF24')
}
.padding(30)
.backgroundColor('rgba(0, 0, 0, 0.7)')
.borderRadius(24)
.backdropBlur(20)
}
}
// 浮动数据面板组件
@Component
struct FloatDataPanel {
@Prop motionHistory: MotionAnalysis[];
@Prop painHistory: PainAssessment[];
@Prop emotionHistory: EmotionState[];
@Prop onClose: () => void;
build() {
Column({ space: 16 }) {
Row() {
Text('📊 康复数据')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Button('✕')
.fontSize(16)
.fontColor('#FFFFFF')
.backgroundColor('transparent')
.onClick(this.onClose)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
// 疼痛趋势图
Column({ space: 8 }) {
Text('疼痛趋势')
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
Canvas(this.drawPainTrend)
.width('100%')
.height(100)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(8)
}
// 活动度趋势
Column({ space: 8 }) {
Text('活动度趋势')
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
Canvas(this.drawRomTrend)
.width('100%')
.height(100)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(8)
}
// 情绪分布
Column({ space: 8 }) {
Text('情绪状态')
.fontSize(14)
.fontColor('rgba(255,255,255,0.8)')
Row({ space: 12 }) {
ForEach(this.getEmotionDistribution(), ([emotion, count]: [string, number]) => {
Column({ space: 4 }) {
Circle()
.width(12)
.height(12)
.fill(this.getEmotionColor(emotion))
Text(`${count}次`)
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)')
}
})
}
}
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('rgba(20, 20, 40, 0.85)')
.borderRadius(20)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
}
private drawPainTrend = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
context.clearRect(0, 0, w, h);
if (this.painHistory.length < 2) return;
// 绘制疼痛趋势线
context.beginPath();
context.moveTo(0, h - (this.painHistory[0].level / 10) * h);
this.painHistory.forEach((pain, index) => {
const x = (index / (this.painHistory.length - 1)) * w;
const y = h - (pain.level / 10) * h;
context.lineTo(x, y);
});
context.strokeStyle = '#EF4444';
context.lineWidth = 2;
context.stroke();
};
private drawRomTrend = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
context.clearRect(0, 0, w, h);
if (this.motionHistory.length < 2) return;
context.beginPath();
context.moveTo(0, h - (this.motionHistory[0].rangeOfMotion / 100) * h);
this.motionHistory.forEach((motion, index) => {
const x = (index / (this.motionHistory.length - 1)) * w;
const y = h - (motion.rangeOfMotion / 100) * h;
context.lineTo(x, y);
});
context.strokeStyle = '#4ADE80';
context.lineWidth = 2;
context.stroke();
};
private getEmotionDistribution(): Map<string, number> {
const distribution = new Map<string, number>();
this.emotionHistory.forEach(e => {
const count = distribution.get(e.primary) || 0;
distribution.set(e.primary, count + 1);
});
return distribution;
}
private getEmotionColor(emotion: string): ResourceColor {
const colors: Map<string, ResourceColor> = new Map([
['calm', '#4ADE80'],
['anxious', '#F59E0B'],
['painful', '#EF4444'],
['tired', '#6B7280'],
['motivated', '#EC4899'],
['frustrated', '#DC2626']
]);
return colors.get(emotion) || '#888888';
}
}
// 浮动医生面板组件
@Component
struct FloatDoctorPanel {
@Prop doctorName: string;
@Prop onClose: () => void;
build() {
Column({ space: 12 }) {
Row() {
Text(`👨⚕️ ${this.doctorName}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Button('✕')
.fontSize(14)
.fontColor('#FFFFFF')
.backgroundColor('transparent')
.onClick(this.onClose)
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
// 医生视频区域(简化)
Column()
.width('100%')
.height(200)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.overlay(
Text('医生视频通话')
.fontSize(14)
.fontColor('rgba(255,255,255,0.5)')
)
// 快速消息
Row({ space: 8 }) {
Button('加油!')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('rgba(59, 130, 246, 0.5)')
.borderRadius(16)
Button('休息一下')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('rgba(34, 197, 94, 0.5)')
.borderRadius(16)
Button('调整强度')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('rgba(249, 115, 22, 0.5)')
.borderRadius(16)
}
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('rgba(20, 20, 40, 0.85)')
.borderRadius(20)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
}
}
五、关键技术总结
5.1 Face AR在康复中的适配清单
| 适配项 | 说明 | 代码位置 |
|---|---|---|
| 疼痛评估精度 | 综合皱眉、眯眼、鼻皱、咬牙等微表情 | PainEmotionSystem.assessPain() |
| 情绪稳定性分析 | 连续10帧表情一致性检测 | PainEmotionSystem.assessEmotion() |
| 疲劳度监测 | 眨眼频率 + 眼神呆滞检测 | FaceExpression.eyeBlink |
| 隐私保护 | 端侧处理,不上传云端 | module.json5权限声明 |
5.2 Body AR动作追踪最佳实践
| 实践项 | 说明 | 代码位置 |
|---|---|---|
| 关节角度计算 | 向量夹角法计算关节活动度 | RehabMotionSystem.measureJointAngle() |
| 对称性评估 | 左右关节角度差异分析 | RehabMotionSystem.calculateSymmetry() |
| 重复次数检测 | 动作峰值检测 + 保持时间验证 | RehabMotionSystem.countRepetitions() |
| 安全范围警示 | 关节角度超出预设范围时告警 | JointAngle.isSafe |
5.3 沉浸光感康复适配要点
- 阶段色动态切换:急性期冷静蓝、恢复期愈合绿、强化期活力橙、维持期稳定白
- 情绪响应光效:焦虑时柔和紫蓝安抚、疼痛时淡粉缓解、积极时亮度提升
- 安全警示机制:动作超出安全范围时标题栏快速闪烁红色警示
- 护眼模式:训练超过30分钟或检测到疲劳时自动降低蓝光和亮度
六、调试与测试建议
6.1 AR性能监控
// 在康复循环中添加性能监控
const startTime = performance.now();
// ... AR处理逻辑
const processTime = performance.now() - startTime;
if (processTime > 33) {
console.warn(`AR处理帧耗时${processTime.toFixed(1)}ms,存在掉帧风险`);
}
6.2 多窗口测试矩阵
| 测试场景 | 预期结果 |
|---|---|
| 主窗口全屏 + 浮动数据面板 | 标题栏光效同步,面板不遮挡训练画面 |
| 分屏模式(左训练右医生视频) | 悬浮导航自动适配宽度,功能按钮不重叠 |
| 外接显示器扩展 | 多窗口可拖拽至副屏,光效状态同步 |
| 疼痛等级>7时 | 环境光效自动切换为安抚模式,亮度降低 |
6.3 常见问题排查
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 疼痛评估不准确 | 摄像头角度导致面部遮挡 | 调整摄像头至正面平视角度 |
| 关节角度测量偏差 | 人体与摄像头距离过远 | 建议患者站在摄像头前1.5-2米处 |
| 光效切换闪烁 | 动画时长过短 | 调整duration至1500ms以上 |
| 多窗口光效不同步 | AppStorage键名不一致 |
统一使用current_rehab_theme作为同步键 |
七、总结与展望
本文基于HarmonyOS 6(API 23)的悬浮导航、沉浸光感与Face AR & Body AR特性,完整实战了一款PC端"灵犀康养"智能康复系统。核心创新点总结:
- 阶段感知光效系统:根据康复阶段动态切换主题色,急性期冷静蓝缓解焦虑、恢复期愈合绿促进愈合、强化期活力橙激发动力、维持期稳定白保持状态
- Face AR疼痛评估:利用64种BlendShape参数实时评估疼痛等级(0-10分),识别疼痛类型(尖锐/钝痛/灼烧/酸痛/搏动性)
- 情绪驱动康复调节:检测到焦虑时自动切换安抚光效、检测到疼痛时降低训练强度、检测到积极情绪时适当提升挑战
- Body AR动作精准追踪:通过20+骨骼关键点追踪,实时测量关节角度、评估动作对称性、计数重复次数、评估动作质量
- 悬浮导航自适应:采用
HdsTabs悬浮样式,四周留白,支持透明度三档调节,最大化训练画面区域 - PC级多窗口协作:主训练窗口 + 浮动动作指导 + 浮动数据面板 + 浮动医生视频 + 浮动家属关怀,通过
WindowManager实现跨窗口光效联动
未来扩展方向:
- AI康复处方:结合患者历史数据,AI自动生成个性化康复方案
- 分布式康复监护:通过鸿蒙分布式软总线,实现家中康复设备 → 社区康复中心 → 医院康复科的多级联动
- 家属远程关怀:家属通过智慧屏实时查看患者康复状态,发送鼓励消息
- 虚拟现实融合:结合VR头显,将康复训练转化为虚拟游戏场景,提升患者依从性
- 康复大数据:汇聚匿名化康复数据,训练更精准的疼痛评估和动作识别模型
转载自:https://blog.csdn.net/u014727709/article/details/161020889
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐



所有评论(0)