开源鸿蒙跨平台Flutter开发:昼夜节律与睡眠相位-脑电波周期与最佳苏醒测绘架构
文章摘要: 本文介绍了一个基于开源鸿蒙跨平台社区的睡眠周期监测系统,利用Flutter技术构建硬核的昼夜节律测绘平台。系统通过脑电波物理模型(NREM/REM周期)和数学公式(如睡眠惯性指数衰减方程)精确计算最佳苏醒时间点,避免深度睡眠被中断导致的神经元损伤。核心创新包括:90分钟相位槽建模、复合脑电波Canvas渲染引擎、自适应时间追踪机制,以及防溢出的响应式界面设计。代码实现展示了相位演算、波
欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net
示例效果



1. 导论:睡眠惯性与脑电频段的物理坍缩
在传统的健康监控系统中,睡眠管理通常被简化为一条线性的一维倒计时。然而,现代脑神经科学与睡眠物理学表明,人类的睡眠绝不是一个均质的过程,而是一个由 N R E M NREM NREM(非快速眼动期)与 R E M REM REM(快速眼动期)交替形成的振荡波段,每个周期严格锁定在约为 90 90 90 分钟的物理节律内。
当人类在深度睡眠(Deep Sleep,脑电频率处于低频高幅的 Δ \Delta Δ 波状态)时被强行唤醒,大脑将经历极其严重的“睡眠惯性(Sleep Inertia)”,这是一种类似在深海中被瞬间拉出水面的脑神经减压综合征。本架构工程彻底抛弃了扁平的闹钟思维,利用 Flutter 的 CustomPainter 与 AnimationController,建立了一套极度硬核的“昼夜节律与睡眠相位测绘台”。系统通过推演复杂的脑波波动函数,直击睡眠周期的波谷与波峰,精准测算出能让神经元物理状态损失最小的最佳“苏醒相位点”。
2. 生物钟数据建模:90分钟相位的非线性周期
要在代码层面防御睡眠剥夺与睡眠惯性,必须建立精确的离散数学映射模型,将人类的睡眠分解为 90 90 90 分钟的相位槽(Phase Slots)。
2.1 睡眠惯性的指数衰减方程
如果我们在时间点 t t t 醒来,所受到的睡眠惯性强度 I ( t ) I(t) I(t) 取决于距离上一次 REM 睡眠期结束的时间 t r e m t_{rem} trem。在理想状态下,我们构建了以下卡玛拉衰减函数用于判定苏醒风险:
I ( t ) = I 0 ⋅ e − k ( t − t r e m ) ⋅ δ ( Stage ) I(t) = I_0 \cdot e^{-k(t - t_{rem})} \cdot \delta(\text{Stage}) I(t)=I0⋅e−k(t−trem)⋅δ(Stage)
其中,当处于 Deep Sleep 阶段时, δ \delta δ 系数达到最大值;而处于 REM 阶段末尾时, δ ≈ 0 \delta \approx 0 δ≈0。
2.2 相位槽结构体的解耦设计
在系统架构中,我们提取出了包含了时间常量与循环计算的状态机类:
通过内置 14 14 14 分钟的 fallAsleepLatency(入睡潜伏期偏置),我们将每一个睡眠实体 SleepCycleOption 的目标唤醒点强制对齐在第 3 3 3 乃至第 6 6 6 个物理周期的末端波峰处。
3. 极客频段引擎:多模态正弦节律的拓扑渲染
为了展现脑电波(EEG)在不同睡眠相位下的剧烈波动,我们启用了 Canvas 引擎对脑波序列进行了实时光栅化渲染。这绝非简单的正弦曲线,而是一个叠加了深度衰减与高频扰动的复合干涉波。
3.1 复合频段干涉公式
在 BrainWavePainter 引擎内部,我们为整个坐标系的横轴(时间进度 x x x)映射了以下连续分段波动方程 Ψ ( x ) \Psi(x) Ψ(x):
Ψ ( x ) = A b a s e ( x ) + ∑ n = 1 N [ A d ( x ) sin ( 2 π f d x ) + A r e m ( x ) cos ( 2 π f r e m x + ϕ ( t ) ) ] \Psi(x) = A_{base}(x) + \sum_{n=1}^{N} \Big[ A_d(x) \sin(2\pi f_d x) + A_{rem}(x) \cos(2\pi f_{rem} x + \phi(t)) \Big] Ψ(x)=Abase(x)+n=1∑N[Ad(x)sin(2πfdx)+Arem(x)cos(2πfremx+ϕ(t))]
在进入“深度睡眠区(Deep Sleep)”时,振幅 A d A_d Ad 放大而频率 f d f_d fd 降低,模拟出低沉平缓的 Δ \Delta Δ 脑波;而在跨入“REM(苏醒窗)”时,受到时间参数 t t t 的动画影响, A r e m A_{rem} Arem 发生高频位移,呈现出快速眼动期的脑电抖动与活跃态。
3.2 自适应时间追踪与状态机偏置
系统内部同时开启了基于 Timer.periodic 的时钟流处理系统。如果没有人为覆盖入睡基点,系统将每隔 60 60 60 秒重新向状态机发送一次真实世界的时间戳,并推动所有右侧面板内的推荐节点发生雪崩式链式位移更新,保证用户获得的都是“如果我现在立刻闭眼”的最精确反馈。
4. 核心代码解构与极客实现法则
本架构中的代码不仅处理了繁杂的极坐标变换,更在布局与动画防爆层面上做出了卓越的防御。以下是四段封神级别的核心代码解构。
4.1 苏醒相位演算状态闭包
这是整个生物节律运算的物理内核,将分钟级别偏移严格卡在相位点:
for (int i = 1; i <= 7; i++) {
int totalSleep = i * cycleDuration;
int wakeMinutes = baseMinutes + totalSleep;
int wakeHour = (wakeMinutes ~/ 60) % 24;
int wakeMinute = wakeMinutes % 60;
SleepQuality q;
if (i < 3) {
q = SleepQuality.deprived;
} else if (i >= 4 && i <= 6) {
q = SleepQuality.optimal;
} else {
q = SleepQuality.grogginess; // 模拟过长睡眠或处于非最佳苏醒区
}
options.add(SleepCycleOption(
cycles: i,
通过简单的整除模运算 % 24,极其干净地跨越了午夜的时间壁垒,并完成了多枚举维度的分类打击。
4.2 响应式界面引擎防溢出机制
延续对屏幕空间坍缩的管理,这里严格限制了 CustomScrollView 的装配边界:
if (isCompact) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 380,
child: _buildBrainWaveAnalyzer(isCompact: true),
),
),
SliverToBoxAdapter(
child: Container(height: 1, color: const Color(0xFF1E1E2E)),
),
SliverToBoxAdapter(
child: _buildControlPanel(),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildCycleCard(_calculatedOptions[index]);
},
在窄屏状态下,将顶部脑波图与底部的动态卡片列表整合至同一纵向滑动槽道中,直接物理阻断了任何纵向 OVERFLOWED 的可能性。
4.3 高维分段睡眠波形方程生成
在 Canvas 渲染器中,这一段利用数学强行挤压出了深度睡眠的低频曲线与 REM 的高频抖动:
final double cyclePhase = progress % 1.0;
double y;
// Shape the sleep hypnogram using piecewise or a skewed trig function
if (cyclePhase < 0.2) {
// Falling asleep (Wake to Deep)
y = ui.lerpDouble(wakeY, deepY, cyclePhase / 0.2)!;
} else if (cyclePhase < 0.6) {
// Deep sleep bottom
y = deepY - math.sin((cyclePhase - 0.2) / 0.4 * math.pi) * 20; // Small fluctuation at bottom
} else if (cyclePhase < 0.8) {
// Rising to REM
y = ui.lerpDouble(deepY, remY, (cyclePhase - 0.6) / 0.2)!;
} else {
// REM Phase
y = remY + math.sin((cyclePhase - 0.8) / 0.2 * math.pi * 2) * 10;
// Add a high-frequency jitter to represent REM brain waves, moving with animation
y += math.sin((x + animationValue * 100) * 0.5) * 5;
}
// Tweak depth based on cycle index (later cycles have less deep sleep, more REM)
final int currentCycleIndex = progress.floor();
final double depthAttenuation = currentCycleIndex * 15.0; // Deep sleep gets shallower
这里通过 progress % 1.0 在时间域内斩断 90 90 90 分钟的循环,并在后半夜(currentCycleIndex 增大时)通过 depthAttenuation 逐渐削弱深度睡眠区,完美贴合人类睡眠前沉后浅的生物节律。
4.4 最佳苏醒窗口脉冲渲染器
为了引导用户直击最薄弱的苏醒防线,系统在波峰尽头注入了高亮脉冲点阵:
// Draw optimal wake points (pulsing)
final Paint pointPaint = Paint()
..color = const Color(0xFF00E5FF)
..style = PaintingStyle.fill;
final Paint pulsePaint = Paint()
..color = const Color(0xFF00E5FF).withValues(alpha: 0.3 * (1 - animationValue))
..style = PaintingStyle.fill;
for (final pt in optimalWakePoints) {
canvas.drawCircle(pt, 4, pointPaint);
canvas.drawCircle(pt, 4 + animationValue * 15, pulsePaint);
随着 animationValue 的线性推动,半透明的脉冲层向外极速扩张并消散,产生如同雷达信号般极具科技压迫感的科幻呼吸特效。
5. 结语:在比特海中测量意识坍缩
我们不仅在创造一个应用,更是在挑战人类生命体征数字化的极限。“昼夜节律与睡眠相位测绘台”打破了死板的数字时钟边界,用最底层、最纯粹的 Canvas 画布指令集硬生生勾勒出了在神经元层面发生的波形更迭。通过对 90 90 90 分钟相位的精算捕获与衰减函数渲染,系统把虚无缥缈的“最佳苏醒点”具象化为发光的深海脉冲雷达。当跨越平台的技术不再受限于控件的禁锢时,工业级代码就能在睡眠神经学的这片星辰大海中劈波斩浪!
完整代码
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
DeviceOrientation.portraitUp,
]);
runApp(const CircadianSleepApp());
}
class CircadianSleepApp extends StatelessWidget {
const CircadianSleepApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '昼夜节律与睡眠相位测绘台',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF03030A), // 极夜深渊黑
useMaterial3: true,
fontFamily: 'Roboto',
),
home: const SleepPhaseDashboard(),
);
}
}
// ==========================================
// 领域模型 (Domain Models)
// ==========================================
enum SleepQuality {
optimal, // 极佳 (周期末尾苏醒)
grogginess, // 睡眠惯性 (深度睡眠被唤醒)
deprived, // 睡眠剥夺 (周期太少)
}
extension SleepQualityExt on SleepQuality {
Color get color {
switch (this) {
case SleepQuality.optimal:
return const Color(0xFF00E5FF); // 霓虹青
case SleepQuality.grogginess:
return const Color(0xFFEAB308); // 警告黄
case SleepQuality.deprived:
return const Color(0xFFEF4444); // 剥夺红
}
}
String get label {
switch (this) {
case SleepQuality.optimal:
return '相位重合 (Optimal)';
case SleepQuality.grogginess:
return '睡眠惯性 (Grogginess)';
case SleepQuality.deprived:
return '周期不足 (Deprived)';
}
}
}
class SleepCycleOption {
final int cycles;
final int totalMinutes; // 包含入睡时间
final TimeOfDay wakeTime;
final SleepQuality quality;
SleepCycleOption({
required this.cycles,
required this.totalMinutes,
required this.wakeTime,
required this.quality,
});
String get durationString {
final int h = totalMinutes ~/ 60;
final int m = totalMinutes % 60;
return '${h}h ${m}m';
}
}
// ==========================================
// 核心状态控制面板 (Main Dashboard)
// ==========================================
class SleepPhaseDashboard extends StatefulWidget {
const SleepPhaseDashboard({super.key});
@override
State<SleepPhaseDashboard> createState() => _SleepPhaseDashboardState();
}
class _SleepPhaseDashboardState extends State<SleepPhaseDashboard> with TickerProviderStateMixin {
TimeOfDay? _selectedBedTime;
List<SleepCycleOption> _calculatedOptions = [];
late AnimationController _brainWaveController;
Timer? _realtimeTimer;
TimeOfDay _currentTime = TimeOfDay.now();
static const int fallAsleepLatency = 14; // 平均入睡潜伏期 14 分钟
static const int cycleDuration = 90; // 睡眠周期 90 分钟
@override
void initState() {
super.initState();
_brainWaveController = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
_realtimeTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
setState(() {
_currentTime = TimeOfDay.now();
if (_selectedBedTime == null) {
_calculateCycles(_currentTime);
}
});
});
_calculateCycles(_currentTime);
}
@override
void dispose() {
_brainWaveController.dispose();
_realtimeTimer?.cancel();
super.dispose();
}
void _calculateCycles(TimeOfDay bedTime) {
List<SleepCycleOption> options = [];
int baseMinutes = bedTime.hour * 60 + bedTime.minute + fallAsleepLatency;
for (int i = 1; i <= 7; i++) {
int totalSleep = i * cycleDuration;
int wakeMinutes = baseMinutes + totalSleep;
int wakeHour = (wakeMinutes ~/ 60) % 24;
int wakeMinute = wakeMinutes % 60;
SleepQuality q;
if (i < 3) {
q = SleepQuality.deprived;
} else if (i >= 4 && i <= 6) {
q = SleepQuality.optimal;
} else {
q = SleepQuality.grogginess; // 模拟过长睡眠或处于非最佳苏醒区
}
options.add(SleepCycleOption(
cycles: i,
totalMinutes: totalSleep + fallAsleepLatency,
wakeTime: TimeOfDay(hour: wakeHour, minute: wakeMinute),
quality: q,
));
}
// 只保留 3-6 个周期作为主要建议
setState(() {
_calculatedOptions = options.where((o) => o.cycles >= 3 && o.cycles <= 6).toList();
// 反转,让最长睡眠时间在最上面
_calculatedOptions = _calculatedOptions.reversed.toList();
});
}
Future<void> _selectTime(BuildContext context) async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: _selectedBedTime ?? TimeOfDay.now(),
builder: (context, child) {
return Theme(
data: ThemeData.dark().copyWith(
colorScheme: const ColorScheme.dark(
primary: Color(0xFF00E5FF),
onPrimary: Colors.black,
surface: Color(0xFF0A0A1A),
onSurface: Colors.white,
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
_selectedBedTime = picked;
_calculateCycles(picked);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxWidth < 900;
if (isCompact) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 380,
child: _buildBrainWaveAnalyzer(isCompact: true),
),
),
SliverToBoxAdapter(
child: Container(height: 1, color: const Color(0xFF1E1E2E)),
),
SliverToBoxAdapter(
child: _buildControlPanel(),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildCycleCard(_calculatedOptions[index]);
},
childCount: _calculatedOptions.length,
),
),
],
);
}
return Row(
children: [
Expanded(
flex: 4,
child: Column(
children: [
_buildControlPanel(),
Expanded(
child: ListView.builder(
itemCount: _calculatedOptions.length,
itemBuilder: (context, index) {
return _buildCycleCard(_calculatedOptions[index]);
},
),
),
],
),
),
Container(width: 1, color: const Color(0xFF1E1E2E)),
Expanded(
flex: 5,
child: _buildBrainWaveAnalyzer(isCompact: false),
),
],
);
},
),
);
}
Widget _buildControlPanel() {
final displayTime = _selectedBedTime ?? _currentTime;
return Container(
color: const Color(0xFF050510),
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.waves, color: Color(0xFF00E5FF), size: 28),
const SizedBox(width: 12),
const Text(
'相位苏醒测绘引擎',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 2,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF6D28D9).withValues(alpha: 0.2),
border: Border.all(color: const Color(0xFF6D28D9)),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'REM PHASE',
style: TextStyle(
color: Color(0xFFC4B5FD),
fontWeight: FontWeight.bold,
fontSize: 12,
letterSpacing: 1,
),
),
),
],
),
const SizedBox(height: 32),
Text(
'假设入睡节点 (Bedtime Node)',
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 12),
InkWell(
onTap: () => _selectTime(context),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF0A0A1A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1E1E2E)),
),
child: Row(
children: [
Text(
displayTime.format(context),
style: const TextStyle(
fontSize: 42,
fontWeight: FontWeight.w900,
color: Colors.white,
fontFamily: 'monospace',
),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
_selectedBedTime == null ? 'REAL-TIME SYNC' : 'MANUAL OVERRIDE',
style: TextStyle(
fontSize: 10,
color: _selectedBedTime == null ? const Color(0xFF10B981) : const Color(0xFFEAB308),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Icon(Icons.edit_calendar, color: Colors.grey, size: 24),
],
),
],
),
),
),
const SizedBox(height: 16),
Row(
children: [
Icon(Icons.info_outline, color: Colors.grey[600], size: 14),
const SizedBox(width: 8),
Text(
'已硬编码包含 $fallAsleepLatency 分钟的入睡潜伏期偏置。',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
],
),
);
}
Widget _buildCycleCard(SleepCycleOption option) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF0A0A1A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: option.quality.color.withValues(alpha: 0.3)),
boxShadow: [
BoxShadow(
color: option.quality.color.withValues(alpha: 0.05),
blurRadius: 20,
spreadRadius: -5,
)
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: option.quality.color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: option.quality.color,
blurRadius: 8,
)
],
),
),
const SizedBox(width: 12),
Text(
'TARGET WAKE TIME',
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
],
),
const SizedBox(height: 8),
Text(
option.wakeTime.format(context),
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w900,
color: option.quality.color,
fontFamily: 'monospace',
),
),
],
),
),
Container(
padding: const EdgeInsets.only(left: 20),
decoration: const BoxDecoration(
border: Border(left: BorderSide(color: Color(0xFF1E1E2E))),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${option.cycles} CYCLES',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
option.durationString,
style: TextStyle(
fontSize: 14,
color: Colors.grey[400],
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: option.quality.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
option.quality.label,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: option.quality.color,
),
),
),
],
),
),
],
),
);
}
Widget _buildBrainWaveAnalyzer({required bool isCompact}) {
return Container(
color: const Color(0xFF03030A),
child: Stack(
children: [
// Background Grid
CustomPaint(
size: Size.infinite,
painter: GridMatrixPainter(),
),
// Brain Wave Rendering
AnimatedBuilder(
animation: _brainWaveController,
builder: (context, child) {
return CustomPaint(
size: Size.infinite,
painter: BrainWavePainter(
animationValue: _brainWaveController.value,
cycles: _calculatedOptions.isNotEmpty ? _calculatedOptions.first.cycles : 6,
),
);
},
),
// Overlay Texts
Positioned(
top: 32,
left: 32,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'NREM / REM 波函数干涉拓扑',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
Text(
'ΔT = $cycleDuration MIN / Phase',
style: TextStyle(
color: const Color(0xFF00E5FF),
fontSize: 12,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
],
),
),
// Legend
Positioned(
bottom: 32,
left: 32,
child: Row(
children: [
_buildLegendItem(const Color(0xFF00E5FF), 'REM (苏醒窗)'),
const SizedBox(width: 16),
_buildLegendItem(const Color(0xFF6D28D9), 'DEEP (睡眠惯性区)'),
],
),
),
],
),
);
}
Widget _buildLegendItem(Color color, String label) {
return Row(
children: [
Container(
width: 12,
height: 4,
decoration: BoxDecoration(
color: color,
boxShadow: [
BoxShadow(color: color, blurRadius: 6),
],
),
),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
color: Colors.grey[400],
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
],
);
}
}
// ==========================================
// 极客物理渲染:背景坐标系与波形引擎
// ==========================================
class GridMatrixPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF1E1E2E).withValues(alpha: 0.5)
..strokeWidth = 1.0;
const double spacing = 40.0;
// Vertical lines
for (double i = 0; i < size.width; i += spacing) {
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paint);
}
// Horizontal lines
for (double i = 0; i < size.height; i += spacing) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paint);
}
// Y-Axis labels (Stages)
final textPainter = TextPainter(textDirection: TextDirection.ltr);
final stages = ['WAKE', 'REM', 'LIGHT', 'DEEP'];
for (int i = 0; i < stages.length; i++) {
textPainter.text = TextSpan(
text: stages[i],
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
);
textPainter.layout();
final double yPos = (size.height / 5) * (i + 1) - textPainter.height / 2;
textPainter.paint(canvas, Offset(8, yPos));
// Draw sub-axis
final axisPaint = Paint()
..color = const Color(0xFF1E1E2E)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
final path = Path()
..moveTo(40, yPos + textPainter.height / 2)
..lineTo(size.width, yPos + textPainter.height / 2);
canvas.drawPath(path.dashPath([5, 5]), axisPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// Helper extension for dash path
extension PathExtension on Path {
Path dashPath(List<double> dashArray) {
Path dest = Path();
for (final metric in computeMetrics()) {
double distance = 0.0;
bool draw = true;
int index = 0;
while (distance < metric.length) {
final double len = dashArray[index % dashArray.length];
if (draw) {
dest.addPath(metric.extractPath(distance, distance + len), Offset.zero);
}
distance += len;
draw = !draw;
index++;
}
}
return dest;
}
}
class BrainWavePainter extends CustomPainter {
final double animationValue;
final int cycles;
BrainWavePainter({
required this.animationValue,
required this.cycles,
});
@override
void paint(Canvas canvas, Size size) {
if (size.width <= 0 || size.height <= 0) return;
final double startX = 60.0;
final double endX = size.width - 20.0;
final double graphWidth = endX - startX;
// Defined Y levels
final double wakeY = (size.height / 5) * 1;
final double remY = (size.height / 5) * 2;
final double deepY = (size.height / 5) * 4;
final Path wavePath = Path();
final List<Offset> optimalWakePoints = [];
// The length of one cycle in pixels
final double cycleWidth = graphWidth / cycles;
for (double x = 0; x <= graphWidth; x++) {
// Calculate progress across all cycles
final double progress = x / cycleWidth;
// We want a wave that starts at wakeY, drops to deepY, comes back up to remY.
// A cycle goes: Wake -> Light -> Deep -> Light -> REM.
// Using a skewed cosine wave to simulate the hypnogram.
final double cyclePhase = progress % 1.0;
double y;
// Shape the sleep hypnogram using piecewise or a skewed trig function
if (cyclePhase < 0.2) {
// Falling asleep (Wake to Deep)
y = ui.lerpDouble(wakeY, deepY, cyclePhase / 0.2)!;
} else if (cyclePhase < 0.6) {
// Deep sleep bottom
y = deepY - math.sin((cyclePhase - 0.2) / 0.4 * math.pi) * 20; // Small fluctuation at bottom
} else if (cyclePhase < 0.8) {
// Rising to REM
y = ui.lerpDouble(deepY, remY, (cyclePhase - 0.6) / 0.2)!;
} else {
// REM Phase
y = remY + math.sin((cyclePhase - 0.8) / 0.2 * math.pi * 2) * 10;
// Add a high-frequency jitter to represent REM brain waves, moving with animation
y += math.sin((x + animationValue * 100) * 0.5) * 5;
}
// Tweak depth based on cycle index (later cycles have less deep sleep, more REM)
final int currentCycleIndex = progress.floor();
final double depthAttenuation = currentCycleIndex * 15.0; // Deep sleep gets shallower
if (y > deepY - 30) {
y -= depthAttenuation.clamp(0, (deepY - remY) * 0.8);
}
final Offset pt = Offset(startX + x, y);
if (x == 0) {
wavePath.moveTo(pt.dx, pt.dy);
} else {
wavePath.lineTo(pt.dx, pt.dy);
}
// Detect end of cycle for optimal wake points
if ((cyclePhase > 0.98 || x == graphWidth) && currentCycleIndex > 0) {
if (!optimalWakePoints.any((p) => (p.dx - pt.dx).abs() < 10)) {
optimalWakePoints.add(pt);
}
}
}
// Gradient stroke for the wave
final Paint wavePaint = Paint()
..shader = ui.Gradient.linear(
Offset(0, remY),
Offset(0, deepY),
[
const Color(0xFF00E5FF), // REM Color
const Color(0xFF6D28D9), // DEEP Color
],
)
..style = PaintingStyle.stroke
..strokeWidth = 3.0
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
canvas.drawPath(wavePath, wavePaint);
// Draw optimal wake points (pulsing)
final Paint pointPaint = Paint()
..color = const Color(0xFF00E5FF)
..style = PaintingStyle.fill;
final Paint pulsePaint = Paint()
..color = const Color(0xFF00E5FF).withValues(alpha: 0.3 * (1 - animationValue))
..style = PaintingStyle.fill;
for (final pt in optimalWakePoints) {
canvas.drawCircle(pt, 4, pointPaint);
canvas.drawCircle(pt, 4 + animationValue * 15, pulsePaint);
// Draw vertical drop line
final Paint dropPaint = Paint()
..color = const Color(0xFF00E5FF).withValues(alpha: 0.3)
..strokeWidth = 1.0;
canvas.drawLine(pt, Offset(pt.dx, size.height), dropPaint);
}
}
@override
bool shouldRepaint(covariant BrainWavePainter oldDelegate) {
return oldDelegate.animationValue != animationValue ||
oldDelegate.cycles != cycles;
}
}
更多推荐



所有评论(0)