开源鸿蒙跨平台Flutter开发:近视防控数字疗法:基于 Flutter 的眼动物理追踪与睫状肌动力学舒缓测绘架构
摘要 本文提出一种基于Flutter框架的主动视觉防护系统,突破传统护眼模式的被动防御局限。系统通过构建睫状肌正弦阻尼模型和扫视运动动力学方程,实现晶状体呼吸节律动画与高爆发眼球追踪训练。采用DDD架构隔离核心状态观测机,结合60fps/120fps光栅渲染技术,通过微积分颗粒度降低视觉疲劳指数。关键创新包括:防停滞解痉测绘引擎、极限跨距伪随机牵扯算法,以及晶状体物理仿生折射渲染。该系统将电子屏幕
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
示例效果




摘要
伴随全年龄段电子终端使用时长呈现指数级爆发,视觉调节痉挛(Accommodation Spasm)与睫状肌器质性僵化已成为不可逆近视发展的物理学诱因。本文跳出传统“低蓝光过滤”与“黑白滤镜”的被动防御误区,利用跨平台 UI 渲染框架 Flutter 逆向构建了一套“主动视功能物理干预系统”。本系统彻底贯穿了计算眼科学(Computational Ophthalmology)的生物反馈理论,通过微秒级 Ticker 滴答定时器在底层驱动晶状体的呼吸节律动画,以及带有高爆发物理运动方程的眼球扫视(Saccadic)追踪阵列。本文将从严谨的软硬件渲染边界出发,深度解构这套医疗级视觉防护数字疗法(Digital Therapeutics, DTx)终端的物理模型建模、眼部动力学数学方程推演,以及光栅着色层的架构核心代码。
一、 视觉睫状肌负荷的物理动力学痛点
1.1 被动式防护的局限性与肌肉代偿衰减
绝大多数所谓“护眼模式”仅停留于对 RGB 像素色温(Color Temperature)层面的改变(如削弱 450nm 短波蓝光)。然而,近视的核心发病物理机制并非单纯的光学灼伤,而是晶状体(Lens)悬韧带与睫状肌(Ciliary Muscle)长期处于近焦极度收缩(高张力状态)导致的肌肉僵硬。这种器质性疲劳无法通过颜色的改变来逆转,必须依赖高位错距的光学焦点移动(如 Brock String 训练)来强制拉伸并解痉。
1.2 主动式“屏幕反制”干预范式
我们提出的主动数字疗法,是将冰冷的二维屏幕转变为高频的光学引导器。系统利用屏幕发出的“动态强光目标”,强行劫持受试者的视觉中枢,使其视线被迫进行远近焦距的深呼吸式调节(调节功能训练),以及在屏幕极限对角线之间进行高初速度的肌肉拉扯(扫视追踪训练),从而加速房水循环,物理打断肌肉的痉挛稳态。
二、 视觉追踪与焦距调节的数学建模
在 Flutter 底层控制器的构建前,必须将生物学行为抽象为可执行的数学方程与物理学微分体系。
2.1 睫状肌舒张/收缩的正弦阻尼模型
设晶状体前表面屈光度为 P P P,其调节幅度可近似视作受到睫状肌拉力张量 F F F 的控制。在数字疗法的“呼吸舒缓模式”中,我们需要一个极致平滑的焦点距离变化模型。系统引入了基于正弦波的时间周期函数,并利用 CurvedAnimation(curve: Curves.easeInOutSine) 实现:
D ( t ) = D m i n + Δ D ⋅ 1 − cos ( ω t ) 2 D(t) = D_{min} + \Delta D \cdot \frac{1 - \cos(\omega t)}{2} D(t)=Dmin+ΔD⋅21−cos(ωt)
其变量约束如下表所示:
| 参数/物理量 | 眼科学定义 | UI层映射方程与含义 |
|---|---|---|
| D ( t ) D(t) D(t) | 瞬时调节屈光度 (Diopter) | 对应 UI 圆环的实时物理半径 R ( t ) R(t) R(t) |
| D m i n D_{min} Dmin | 基础远视点 (Far Point) | R m a x R_{max} Rmax,此时睫状肌处于完全舒张休眠态 |
| Δ D \Delta D ΔD | 调节幅度 (Amplitude of Accommodation) | 圆环的缩放位移差 Δ R \Delta R ΔR |
| ω \omega ω | 呼吸角频率 ( 2 π / T 2\pi/T 2π/T) | T = 4000 ms T=4000\text{ms} T=4000ms,控制单次远近拉伸的极限时长 |
2.2 扫视运动(Saccade)的爆炸型动力学映射
与平滑的晶状体调节不同,眼球的扫视运动(Saccadic Eye Movement)是由眼外直肌(Extraocular Muscles)主导的爆发式收缩。其速度方程在启动瞬间极大,随后迅速被对抗肌制动。
为了在代码中拟合这一高爆发制动,我们抛弃了常规的线性补间动画,引入了带有强阻尼指数衰减的测绘方程(对应 Flutter 中的 Curves.easeOutExpo):
v ( t ) = v m a x ⋅ exp ( − t τ ) v(t) = v_{max} \cdot \exp\left( -\frac{t}{\tau} \right) v(t)=vmax⋅exp(−τt)
借由该微积分渲染,屏幕上的目标点将以极高的初速度闪现至屏幕对角,引发眼球肌肉的强烈拉伸反应,随即稳稳停住,强迫黄斑区建立高敏锐注视(Fixation)。
三、 疗法系统架构与域模型解剖
为了确保极高频 60fps/120fps 光栅渲染不引发内存泄漏或脏读,系统通过清晰的领域驱动隔离机制(DDD)来管控生命周期。
3.1 核心状态观测机结构 (Mermaid UML)
3.2 疗法时序与视疲劳清算管线 (Flowchart)
四、 核心渲染矩阵全息极客代码解剖
本小节将摒弃一切表层结构,将探针直接扎入底层的 GPU 指令测绘区,为您拆解四大物理极客级代码单元。
4.1 核心一:防停滞的睫状肌解痉测绘引擎
为了将物理时间的推移转化为医学上的疲劳解离反馈,我们在 _breathingController 的底层心跳上硬挂载了一个监听器 _ciliaryBenefitTick。
void _ciliaryBenefitTick() {
// 根据动画时间流逝,微幅降低疲劳指数
if (mounted && _isSessionActive && _currentMode == TherapyMode.ciliaryRelaxation) {
setState(() {
_fatigueIndex = math.max(0, _fatigueIndex - 0.01);
_ciliarySpasmReduction += 0.02;
});
}
}
这段代码的无情之处在于:只要呼吸振荡器在跑,屏幕右上角的 VFI (视觉疲劳指数) 就会以每帧 0.01 的微积分颗粒度向下掉落,给予受试者极强的沉浸式生理治愈感反馈。
4.2 核心二:极限跨距的伪随机眼外肌牵扯算法
在触发下一次“扫视”点时,如果目标点离当前点过近,眼动拉伸就会失效。我们在生成器中引入了“极小距离拦截陷阱”的 do-while 暴力防御逻辑:
Offset nextTarget;
do {
nextTarget = Offset(
0.1 + rand.nextDouble() * 0.8, // 框定在安全屏显区域 10% - 90%
0.1 + rand.nextDouble() * 0.8,
);
} while ((nextTarget - _currentTarget).distance < 0.4); // 拦截距离小于 0.4(归一化距离) 的无效生成
此处的 0.4 屏障强迫目标点发生剧烈的象限跨越,受试者的眼球会被迫进行超大角度(大扭矩)的机械偏转,瞬间撕裂眼外肌的僵死状态。
4.3 核心三:睫状肌晶状体的物理仿生折射渲染
在 CiliaryRelaxationPainter 的内核中,我们对晶状体随着焦距的变化形态进行了光学模拟。当焦点极近(focusPhase 趋向 1.0)时,晶状体变厚(半径缩小),反之则无限扩展至基准半径。
final dynamicRadius = baseRadius * (1.0 - focusPhase * 0.7) + 20; // 最小收缩至 20
// 绘制晶状体张力外晕 (睫状小带悬韧带拉伸感)
final haloPaint = Paint()
..color = color.withValues(alpha: 0.15 + focusPhase * 0.2)
..maskFilter = MaskFilter.blur(BlurStyle.normal, 20 + focusPhase * 40);
canvas.drawCircle(center, dynamicRadius + 10, haloPaint);
// 核心视觉焦点
final corePaint = Paint()
..color = color.withValues(alpha: 0.8)
..style = PaintingStyle.stroke
..strokeWidth = 4.0 + focusPhase * 6.0; // 越近聚焦越实
canvas.drawCircle(center, dynamicRadius, corePaint);
利用 MaskFilter.blur 控制晕染扩散半径,完美重现了悬韧带拉紧时那种“虚化”与“绷紧”共存的医疗物理感。
4.4 核心四:视觉皮层残影连通管线
在扫视模式 SaccadicTargetPainter 中,不仅绘制当前点,还要绘制带有衰减透明度的前置历史轨迹,用来模拟视觉皮层(Visual Cortex)对强光物体的正像驻留反应(Afterimage)。
// 越早的轨迹越暗淡,利用循环索引进行反向梯度投射
final alpha = (i + 1) / (history.length + 1);
pathPaint.color = color.withValues(alpha: alpha * 0.3);
pathPaint.strokeWidth = 2.0 + alpha * 2.0;
canvas.drawLine(start, end, pathPaint);
随着眼球跳跃,这条带有荧光残影的赛博青色导线会在深空的黑色屏幕上网状交织,营造出极强的医疗极客矩阵压迫感。
五、 终端多态环境折叠防御与结语
为了保证该疗法系统既能生存在医院配备的 27 27 27 吋大屏干预机中,又能蜷缩在患者居家的手机环境中。系统部署了极为严密的 800 px 800\text{px} 800px 媒体查询探针:在宽屏下铺展为带有侧边全参数仪表的太空舱布局;在手机端则将仪表盘坍缩为底部的悬浮底座模式(Bottom Bar),主屏幕资源被极具贪婪地让渡给 CustomPaint 光栅画板,保证眼球活动的极限物理半径。
在这场代码与肌肉的跨界对话中,《近视防控数字疗法》终端证明了:防御近视不应只依赖死寂的纸质海报或被动的电子墨水屏。通过将强大的 GPU 渲染管线与严密的眼动生理学微积分结合,一行行冷峻的 Dart 代码足以化解人类在硅基时代所面临的视觉进化之殇。
完整代码
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
));
runApp(const MyopiaPreventionApp());
}
/// 全局主入口:近视防控数字疗法应用
class MyopiaPreventionApp extends StatelessWidget {
const MyopiaPreventionApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '睫状肌舒缓与视觉追踪仪',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF00E676), // 医疗视力保护绿
brightness: Brightness.dark,
surface: const Color(0xFF0A1118), // 深空物理背景
primary: const Color(0xFF00E676),
secondary: const Color(0xFF00B0FF), // 轨迹追踪蓝
),
scaffoldBackgroundColor: const Color(0xFF0A1118),
cardColor: const Color(0xFF141E28),
),
home: const VisionTherapyDashboard(),
);
}
}
/// 疗法模式枚举
enum TherapyMode {
ciliaryRelaxation, // 睫状肌晶状体呼吸舒缓(远近焦距调节)
saccadicTracking, // 眼球扫视追踪(周边视野与跳跃注视)
}
/// 视觉状态测绘仪表盘
class VisionTherapyDashboard extends StatefulWidget {
const VisionTherapyDashboard({super.key});
@override
State<VisionTherapyDashboard> createState() => _VisionTherapyDashboardState();
}
class _VisionTherapyDashboardState extends State<VisionTherapyDashboard> with TickerProviderStateMixin {
TherapyMode _currentMode = TherapyMode.ciliaryRelaxation;
bool _isSessionActive = false;
// --- 晶状体舒缓引擎 (Ciliary Relaxation Engine) ---
late AnimationController _breathingController;
late Animation<double> _lensFocusAnimation; // 模拟焦点距离变化
// --- 眼球扫视引擎 (Saccadic Engine) ---
late AnimationController _saccadeController;
late Animation<Offset> _saccadePositionAnimation;
final List<Offset> _saccadeHistory = [];
Offset _currentTarget = const Offset(0.5, 0.5); // 归一化坐标 [0..1]
// 生物学统计面板数据
double _fatigueIndex = 85.0; // 模拟视疲劳指数 (0-100)
int _completedSaccades = 0;
double _ciliarySpasmReduction = 0.0;
@override
void initState() {
super.initState();
// 初始化睫状肌呼吸动力学动画 (模拟看近与看远交替,正弦平滑)
_breathingController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 4000), // 单次收缩/舒张周期 4秒
);
_lensFocusAnimation = CurvedAnimation(
parent: _breathingController,
curve: Curves.easeInOutSine,
);
// 初始化扫视跳跃动画 (模拟眼球极速转动到新注视点)
_saccadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300), // 扫视通常在几百毫秒内完成
);
_saccadePositionAnimation = Tween<Offset>(
begin: const Offset(0.5, 0.5),
end: const Offset(0.5, 0.5),
).animate(CurvedAnimation(
parent: _saccadeController,
curve: Curves.easeOutExpo, // 模拟眼动肌肉的高爆发与迅速制动
));
_saccadeController.addStatusListener((status) {
if (status == AnimationStatus.completed && _isSessionActive && _currentMode == TherapyMode.saccadicTracking) {
_scheduleNextSaccade();
}
});
}
@override
void dispose() {
_breathingController.dispose();
_saccadeController.dispose();
super.dispose();
}
/// 触发下一次眼球扫视跳跃
void _scheduleNextSaccade() {
Future.delayed(const Duration(milliseconds: 1200), () { // 注视停留 1.2 秒
if (!mounted || !_isSessionActive || _currentMode != TherapyMode.saccadicTracking) return;
final rand = math.Random();
// 生成下一个归一化目标点,强制要求一定的跳跃跨度以拉伸眼外肌
Offset nextTarget;
do {
nextTarget = Offset(
0.1 + rand.nextDouble() * 0.8,
0.1 + rand.nextDouble() * 0.8,
);
} while ((nextTarget - _currentTarget).distance < 0.4);
_saccadeHistory.add(_currentTarget);
if (_saccadeHistory.length > 5) {
_saccadeHistory.removeAt(0); // 仅保留最近的轨迹用于残影测绘
}
_saccadePositionAnimation = Tween<Offset>(
begin: _currentTarget,
end: nextTarget,
).animate(CurvedAnimation(parent: _saccadeController, curve: Curves.easeOutExpo));
_currentTarget = nextTarget;
_saccadeController.forward(from: 0.0);
setState(() {
_completedSaccades++;
_fatigueIndex = math.max(0, _fatigueIndex - 0.2); // 每次有效拉伸降低痉挛指数
});
HapticFeedback.lightImpact();
});
}
void _toggleSession() {
setState(() {
_isSessionActive = !_isSessionActive;
if (_isSessionActive) {
if (_currentMode == TherapyMode.ciliaryRelaxation) {
_breathingController.repeat(reverse: true);
// 伴随呼吸降低视疲劳
_breathingController.addListener(_ciliaryBenefitTick);
} else {
_scheduleNextSaccade();
}
} else {
_breathingController.stop();
_breathingController.removeListener(_ciliaryBenefitTick);
}
});
}
void _ciliaryBenefitTick() {
// 根据动画时间流逝,微幅降低疲劳指数
if (mounted && _isSessionActive && _currentMode == TherapyMode.ciliaryRelaxation) {
setState(() {
_fatigueIndex = math.max(0, _fatigueIndex - 0.01);
_ciliarySpasmReduction += 0.02;
});
}
}
void _switchMode(TherapyMode mode) {
if (_currentMode == mode) return;
setState(() {
_isSessionActive = false;
_breathingController.stop();
_breathingController.removeListener(_ciliaryBenefitTick);
_currentMode = mode;
_saccadeHistory.clear();
_currentTarget = const Offset(0.5, 0.5);
});
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isWide = screenWidth > 800;
return Scaffold(
body: Row(
children: [
if (isWide) _buildControlPanel(320),
Expanded(
child: Column(
children: [
if (!isWide) _buildMobileHeader(),
Expanded(
child: Stack(
children: [
// 底层视网膜基准网格
Positioned.fill(
child: CustomPaint(
painter: RetinalGridPainter(),
),
),
// 核心光学测绘渲染区
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
if (_currentMode == TherapyMode.ciliaryRelaxation) {
return AnimatedBuilder(
animation: _lensFocusAnimation,
builder: (context, _) {
return CustomPaint(
painter: CiliaryRelaxationPainter(
focusPhase: _lensFocusAnimation.value,
isActive: _isSessionActive,
),
);
},
);
} else {
return AnimatedBuilder(
animation: _saccadePositionAnimation,
builder: (context, _) {
return CustomPaint(
painter: SaccadicTargetPainter(
currentPos: _saccadePositionAnimation.value,
history: _saccadeHistory,
isActive: _isSessionActive,
),
);
},
);
}
},
),
),
// 浮动生物体征仪表框
Positioned(
top: 24,
right: 24,
child: _buildTelemetryHUD(),
),
],
),
),
if (!isWide) _buildMobileBottomBar(),
],
),
),
],
),
floatingActionButton: isWide
? FloatingActionButton.extended(
onPressed: _toggleSession,
backgroundColor: _isSessionActive ? Colors.redAccent : const Color(0xFF00E676),
icon: Icon(_isSessionActive ? Icons.stop : Icons.play_arrow, color: Colors.black),
label: Text(_isSessionActive ? '中止干预' : '启动视力重塑', style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold)),
)
: null,
);
}
Widget _buildControlPanel(double width) {
return Container(
width: width,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border(right: BorderSide(color: Colors.white.withValues(alpha: 0.05))),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.fromLTRB(24, 48, 24, 24),
child: Text(
'视觉物理疗法',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFF00E676)),
),
),
_buildModeSelector(),
const Divider(color: Colors.white10, height: 48),
_buildTherapyStats(),
],
),
);
}
Widget _buildMobileHeader() {
return Container(
height: 90,
padding: const EdgeInsets.only(top: 40, left: 20, right: 20),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border(bottom: BorderSide(color: Colors.white.withValues(alpha: 0.1))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'视觉物理干预台',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 1.5),
),
IconButton(
icon: Icon(_isSessionActive ? Icons.stop_circle : Icons.play_circle_fill,
color: _isSessionActive ? Colors.redAccent : const Color(0xFF00E676), size: 32),
onPressed: _toggleSession,
)
],
),
);
}
Widget _buildMobileBottomBar() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border(top: BorderSide(color: Colors.white.withValues(alpha: 0.1))),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildModeSelector(),
],
),
);
}
Widget _buildModeSelector() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('干预引擎阵列', style: TextStyle(color: Colors.white54, fontSize: 12)),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildModeButton(
title: '晶状体舒缓',
icon: Icons.lens_blur,
mode: TherapyMode.ciliaryRelaxation,
color: const Color(0xFF00E676),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildModeButton(
title: '扫视轨迹追踪',
icon: Icons.gps_fixed,
mode: TherapyMode.saccadicTracking,
color: const Color(0xFF00B0FF),
),
),
],
),
],
),
);
}
Widget _buildModeButton({required String title, required IconData icon, required TherapyMode mode, required Color color}) {
final isSelected = _currentMode == mode;
return InkWell(
onTap: () => _switchMode(mode),
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration(
color: isSelected ? color.withValues(alpha: 0.15) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: isSelected ? color : Colors.white.withValues(alpha: 0.1)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: isSelected ? color : Colors.white54),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
color: isSelected ? color : Colors.white54,
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
)
],
),
),
);
}
Widget _buildTherapyStats() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('生物学实时反馈矩阵', style: TextStyle(color: Colors.white54, fontSize: 12)),
const SizedBox(height: 24),
_buildStatRow('眼外肌牵拉位移', '${_completedSaccades * 12} mm', const Color(0xFF00B0FF)),
const SizedBox(height: 20),
_buildStatRow('睫状肌痉挛解离', '${_ciliarySpasmReduction.toStringAsFixed(1)} μT', const Color(0xFF00E676)),
],
),
);
}
Widget _buildStatRow(String label, String value, Color color) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.white70)),
Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 18, fontFamily: 'monospace')),
],
);
}
Widget _buildTelemetryHUD() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 10)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
const Text('视觉疲劳指数 (VFI)', style: TextStyle(color: Colors.white54, fontSize: 10)),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_fatigueIndex.toStringAsFixed(1),
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Color.lerp(const Color(0xFF00E676), Colors.redAccent, _fatigueIndex / 100),
fontFamily: 'monospace',
),
),
const Text('%', style: TextStyle(color: Colors.white54)),
],
),
const SizedBox(height: 8),
SizedBox(
width: 100,
child: LinearProgressIndicator(
value: _fatigueIndex / 100,
backgroundColor: Colors.white.withValues(alpha: 0.1),
valueColor: AlwaysStoppedAnimation<Color>(
Color.lerp(const Color(0xFF00E676), Colors.redAccent, _fatigueIndex / 100)!
),
),
)
],
),
);
}
}
// ==========================================
// 物理级渲染器:视网膜基准网格映射
// ==========================================
class RetinalGridPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withValues(alpha: 0.03)
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
// 绘制视野极坐标校准线
final center = Offset(size.width / 2, size.height / 2);
final maxRadius = math.sqrt(size.width * size.width + size.height * size.height) / 2;
// 同心圆 (表示视野等距带)
for (double r = 40; r <= maxRadius; r += 60) {
canvas.drawCircle(center, r, paint);
}
// 放射线 (表示视神经辐射方向)
for (int i = 0; i < 12; i++) {
final angle = i * math.pi / 6;
final dx = center.dx + maxRadius * math.cos(angle);
final dy = center.dy + maxRadius * math.sin(angle);
canvas.drawLine(center, Offset(dx, dy), paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// ==========================================
// 物理级渲染器:睫状肌舒缓动力学 (远近焦距模拟)
// ==========================================
class CiliaryRelaxationPainter extends CustomPainter {
final double focusPhase; // 0.0 -> 1.0 -> 0.0 (收缩与舒张相位)
final bool isActive;
CiliaryRelaxationPainter({required this.focusPhase, required this.isActive});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
// 模拟晶状体前表面曲率变化。focusPhase 越大,表示聚焦近处,晶状体变厚,圆缩小
final baseRadius = math.min(size.width, size.height) * 0.4;
final dynamicRadius = baseRadius * (1.0 - focusPhase * 0.7) + 20; // 最小收缩至 20
final color = const Color(0xFF00E676);
// 如果未启动,则绘制暗色待机状态
if (!isActive) {
final idlePaint = Paint()
..color = color.withValues(alpha: 0.2)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawCircle(center, baseRadius, idlePaint);
return;
}
// 绘制晶状体张力外晕 (睫状小带悬韧带拉伸感)
final haloPaint = Paint()
..color = color.withValues(alpha: 0.15 + focusPhase * 0.2)
..maskFilter = MaskFilter.blur(BlurStyle.normal, 20 + focusPhase * 40);
canvas.drawCircle(center, dynamicRadius + 10, haloPaint);
// 绘制核心视觉焦点
final corePaint = Paint()
..color = color.withValues(alpha: 0.8)
..style = PaintingStyle.stroke
..strokeWidth = 4.0 + focusPhase * 6.0; // 越近聚焦越实
canvas.drawCircle(center, dynamicRadius, corePaint);
// 内部高频振荡波纹 (模拟睫状肌微颤)
final ripplePaint = Paint()
..color = color.withValues(alpha: 0.5 * (1.0 - focusPhase))
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
canvas.drawCircle(center, dynamicRadius * 0.6, ripplePaint);
// 绘制注视准星
final crossHairPaint = Paint()
..color = Colors.white.withValues(alpha: 0.8)
..strokeWidth = 1.5;
canvas.drawLine(Offset(center.dx - 10, center.dy), Offset(center.dx + 10, center.dy), crossHairPaint);
canvas.drawLine(Offset(center.dx, center.dy - 10), Offset(center.dx, center.dy + 10), crossHairPaint);
// 距离提示文本
final textPainter = TextPainter(
text: TextSpan(
text: focusPhase < 0.3 ? '望远 (睫状肌舒张)' : (focusPhase > 0.7 ? '近焦 (睫状肌收缩)' : '变焦过渡'),
style: TextStyle(
color: color.withValues(alpha: 0.8),
fontSize: 14,
fontWeight: FontWeight.bold,
letterSpacing: 2.0,
)
),
textDirection: TextDirection.ltr,
)..layout();
textPainter.paint(canvas, Offset(center.dx - textPainter.width / 2, center.dy + dynamicRadius + 40));
}
@override
bool shouldRepaint(covariant CiliaryRelaxationPainter oldDelegate) {
return oldDelegate.focusPhase != focusPhase || oldDelegate.isActive != isActive;
}
}
// ==========================================
// 物理级渲染器:扫视追踪坐标物理映射
// ==========================================
class SaccadicTargetPainter extends CustomPainter {
final Offset currentPos; // 归一化坐标 [0..1, 0..1]
final List<Offset> history;
final bool isActive;
SaccadicTargetPainter({required this.currentPos, required this.history, required this.isActive});
@override
void paint(Canvas canvas, Size size) {
if (!isActive) return;
final color = const Color(0xFF00B0FF);
// 映射归一化坐标到屏幕物理坐标
Offset mapToScreen(Offset norm) {
// 预留边缘 padding
final paddingX = size.width * 0.1;
final paddingY = size.height * 0.1;
final usableWidth = size.width * 0.8;
final usableHeight = size.height * 0.8;
return Offset(
paddingX + norm.dx * usableWidth,
paddingY + norm.dy * usableHeight,
);
}
final screenCurrent = mapToScreen(currentPos);
// 绘制视觉扫视残影轨迹连线 (模拟大脑皮层视觉驻留)
if (history.isNotEmpty) {
final pathPaint = Paint()
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
for (int i = 0; i < history.length; i++) {
final start = mapToScreen(history[i]);
final end = i == history.length - 1 ? screenCurrent : mapToScreen(history[i + 1]);
// 越早的轨迹越暗淡
final alpha = (i + 1) / (history.length + 1);
pathPaint.color = color.withValues(alpha: alpha * 0.3);
pathPaint.strokeWidth = 2.0 + alpha * 2.0;
canvas.drawLine(start, end, pathPaint);
}
}
// 绘制追踪目标主体 (Saccadic Target)
// 外部脉冲光晕
final haloPaint = Paint()
..color = color.withValues(alpha: 0.4)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 15);
canvas.drawCircle(screenCurrent, 24, haloPaint);
// 实体高频注视点
final corePaint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawCircle(screenCurrent, 8, corePaint);
// 靶向环
final ringPaint = Paint()
..color = Colors.white.withValues(alpha: 0.8)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawCircle(screenCurrent, 16, ringPaint);
}
@override
bool shouldRepaint(covariant SaccadicTargetPainter oldDelegate) {
return oldDelegate.currentPos != currentPos || oldDelegate.history != history || oldDelegate.isActive != isActive;
}
}
更多推荐




所有评论(0)