开源鸿蒙跨平台Flutter开发:儿童语文认知与语义皮层映射引擎_视觉词形区与布洛卡网络渲染架构
摘要: 本文提出基于认知神经语言学的儿童汉字学习交互系统,突破传统偏旁拖拽模式,通过模拟大脑语言处理机制(VWFA、Wernicke、Broca区协同)构建沉浸式学习体验。系统采用Flutter与HarmonyOS跨端技术,将汉字组合过程映射为神经电信号传导动画,运用三次贝塞尔曲线模拟弓状束传导路径,并建立皮层兴奋度衰减模型。通过UML架构与响应式布局设计,实现从神经科学理论到跨平台交互的完整转化
欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net



汉字作为表意文字(Logogram),其字形与语义的绑定极度依赖于大脑左半球枕颞沟的视觉词形区(Visual Word Form Area, VWFA)。当儿童将“氵”与“木”组合为“沐”时,神经冲动会迅速从 VWFA 沿白质纤维束投射至负责语义提取的韦尼克区(Wernicke’s area),最终通过宏伟的弓状束(Arcuate Fasciculus)传导至负责语音运动编码的布洛卡区(Broca’s area)。
本文旨在打破传统早教软件的壁垒,依托 Flutter 强悍的二维标量渲染能力(CustomPainter)与 HarmonyOS 跨端引擎,构建出一套极具赛博生命科学质感的“语文认知与语义皮层映射引擎”。我们将通过二次与三次贝塞尔曲线方程,真实地在屏幕上描绘出大脑语言皮层的运作拓扑,将儿童每一次成功的汉字聚合,具象化为一场穿梭于神经纤维中的电化学风暴。
一、 认知神经语言学模型的数学构筑
为了在代码中完美映射大脑的语言处理过程,我们必须对三个核心皮层的激活阈值与传导机制进行数学抽象。
1. 语言网络功能映射矩阵
| 大脑皮层区域 | 颜色标识 | 生理功能与汉字认知职责 | UI反馈与激活条件 |
|---|---|---|---|
| 视觉词形区 (VWFA) | 赛博深紫 | 正字法解码,辨识偏旁部首的合法组合 | 每次点击选择偏旁时激增(模拟视觉注视) |
| 韦尼克区 (Wernicke) | 突触亮绿 | 词法语义网络检索,理解汉字含义 | 汉字组合成功时,语义被提取,瞬间达峰 |
| 布洛卡区 (Broca) | 运动橘黄 | 语音计划与发音器官运动编码 | 接收到语义信号后,输出拼音时达到峰值 |
2. 神经传导的脉冲流体模型 (KaTeX)
在系统中,当儿童成功将两个部首组合成目标汉字时,会触发一个跨脑区的动作电位传导动画。为了精确控制代表神经信号的高亮粒子在“弓状束(Arcuate Fasciculus)”这种 C 型弯曲纤维中的运动轨迹,我们使用了基于时间参数 t t t 的**三次贝塞尔曲线(Cubic Bézier Curve)**方程:
P ( t ) = ( 1 − t ) 3 P 0 + 3 ( 1 − t ) 2 t P 1 + 3 ( 1 − t ) t 2 P 2 + t 3 P 3 ( 0 ≤ t ≤ 1 ) P(t) = (1-t)^3 P_0 + 3(1-t)^2 t P_1 + 3(1-t) t^2 P_2 + t^3 P_3 \quad (0 \le t \le 1) P(t)=(1−t)3P0+3(1−t)2tP1+3(1−t)t2P2+t3P3(0≤t≤1)
-
定义列表
- P 0 P_0 P0:起点(如 韦尼克区 坐标)。
- P 3 P_3 P3:终点(如 布洛卡区 坐标)。
- P 1 , P 2 P_1, P_2 P1,P2:控制点(位于顶叶附近,用于强制拉伸出弓状的解剖学弧度)。
-
t t t:在引擎中对应
transmissionProgress动画控制器值,控制着动作电位在神经束中滑动的进度。
二、 语义映射引擎的架构拓扑
整个系统的架构围绕着“行为刺激层”、“生化阻尼内稳态层”与“皮层渲染层”展开。
1. 核心类与状态流转拓扑 (UML Class Diagram)
2. 汉字聚合与皮层激活管线 (Flowchart)
三、 核心渲染与状态追踪代码深潜
通过以下四段核心工程代码的详细拆解,我们将彻底展现这套底层架构如何将复杂的生命科学理论转化为丝滑的 Flutter 跨端交互体验。
核心代码一:响应式流式布局与视口抗压 (Sliver Architecture)
为了保证应用在华为 MatePad (宽屏横排) 与普通手机竖屏下均能够达到完美的生化质感呈现,我们继续贯彻基于 LayoutBuilder 和 CustomScrollView 的防爆裂瀑布坍缩原则。
// 核心片段:视口感知与布局坍缩降维
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
// 设置 950px 为生化雷达与控制台分流的临界视口宽度
final isCompact = constraints.maxWidth < 950;
if (isCompact) {
// 窄屏进入降维流式渲染
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 400,
// 顶部保留高精度的左半脑解剖学映射画布
child: _buildBrainVisualizer(isCompact: true),
),
),
SliverToBoxAdapter(
child: Container(height: 1, color: const Color(0xFF1E293B)),
),
SliverToBoxAdapter(
child: _buildMetricsPanel(), // 中部为神经区域能量条槽
),
SliverToBoxAdapter(
child: _buildInteractionConsole(), // 底部容纳偏旁选择舱
),
],
);
}
// 大屏设备下的标准双侧并列布局 (代码略)
},
),
);
}
工程思辨:在这个体系下,所有的汉字区块、能量进度条与底层画布都完全脱离了绝对的像素限制。SliverToBoxAdapter 提供了一个完美的隔离容器,确保在最极端的窄屏长比设备下,组件的渲染树也能依据纵向瀑布流优雅折叠,彻底消灭了越界异常。
核心代码二:内环境稳态阻尼与兴奋度衰减机制
人的大脑是一个高耗能的生物引擎。任何一次外界视觉刺激带来的区域皮层兴奋,都不可能长久维持,而会遵循一套新陈代谢规律迅速跌落至基准线(Base Level)。
// 核心片段:皮层代谢与内环境稳态计时器
void initState() {
super.initState();
// ... 动画控制器初始化
// 生化状态内环境稳态 (衰减机制) 100ms 心跳
_homeostasisTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
bool needUpdate = false;
for (var key in _regions.keys) {
// 若某个脑区 (VWFA/Wernicke/Broca) 的激活水平高于 0.1
if (_regions[key]!.activationLevel > 0.1) {
// 执行固定步长的自然衰减,模拟神经元疲劳与离子泵复位
_regions[key]!.activationLevel -= 0.005;
needUpdate = true;
}
}
// 只有在数据发生真实衰减时才调用 setState 重建渲染树,极大节省性能
if (needUpdate) setState(() {});
});
}
工程思辨:这几行代码是整套应用具备“生命质感”的灵魂。当儿童手指高频点击偏旁部首时,视觉词形区(VWFA)的能量条会随之暴涨,但伴随时间推移又会如潮水般退去。阻尼代谢机制使得系统的数据变化告别了机械的离散跳跃,转而呈现出具有物理质量的平滑过渡。
核心代码三:高精度脑网络解剖拓扑的贝塞尔绘制
在大脑语言皮层映射中,“弓状束(Arcuate Fasciculus)”具有极为特殊的地位。它是一条绕过脑岛与顶叶、连接韦尼克区与布洛卡区的巨大 C 型白质纤维束。直线绘制不仅丑陋,且违背解剖学常识。
// 核心片段:利用三次贝塞尔曲线拟合弓状束纤维
void _drawArcuateFasciculus(Canvas canvas, Offset wernicke, Offset broca, Color c1, Color c2, double activation) {
final Path tractPath = Path();
tractPath.moveTo(wernicke.dx, wernicke.dy);
// 巧妙设置控制点,使曲线向前上方绕行顶叶,再下延至额叶
final double controlX1 = wernicke.dx - (wernicke.dx - broca.dx) * 0.2;
final double controlY1 = broca.dy - 120; // 第一顶叶控制点
final double controlX2 = broca.dx + (wernicke.dx - broca.dx) * 0.2;
final double controlY2 = broca.dy - 80; // 第二下行控制点
// 执行三次贝塞尔曲线闭合
tractPath.cubicTo(controlX1, controlY1, controlX2, controlY2, broca.dx, broca.dy);
// 利用 y 轴微小偏移,绘制多股平行纤维
for (int i = -2; i <= 2; i++) {
final Paint bundlePaint = Paint()
..shader = ui.Gradient.linear(wernicke, broca, [
c1.withValues(alpha: 0.1 + activation * 0.3),
c2.withValues(alpha: 0.1 + activation * 0.3),
])
..style = PaintingStyle.stroke
..strokeWidth = 2.0 + activation * 3.0 // 宽度受皮层激活度干涉
..maskFilter = MaskFilter.blur(BlurStyle.normal, 2.0); // 纤维泛光
canvas.save();
canvas.translate(0, i * 8.0); // 制造纤维束的粗度堆叠
canvas.drawPath(tractPath, bundlePaint);
canvas.restore();
}
}
工程思辨:通过 cubicTo 指令,我们无需引入复杂的 3D 模型渲染库,仅使用极低的性能消耗便在 Canvas 上重现了具备空间层次感的大脑白质网络。多股路径在纵轴上的平移(translate),在配合透明度梯度渲染后,能完美拟合出生物组织的复杂纹理。
核心代码四:跨脑区电化学脉冲的高速粒子物理
当汉字偏旁聚合成功(如“日”+“月”=“明”)的一瞬间,代表语义理解成功的信息流,将高速掠过刚才绘制好的贝塞尔曲线。
// 核心片段:沿非线性曲线推演粒子坐标,绘制信息传递流
void _drawTransmissionParticles(Canvas canvas, Offset vwfa, Offset wernicke, Offset broca, double progress) {
Offset currentPos;
Color pColor;
// 采用分段函数,将进度拆解为两个部分:VWFA -> Wernicke, Wernicke -> Broca
if (progress <= 0.5) {
// 进度拉伸到 0.0 - 1.0 的满量程 (段一)
double t = progress * 2.0;
// 计算二次贝塞尔控制点
final double controlX = (vwfa.dx + wernicke.dx) / 2 + (wernicke.dy - vwfa.dy) * 0.2;
final double controlY = (vwfa.dy + wernicke.dy) / 2 - (wernicke.dx - vwfa.dx) * 0.2;
// 二次贝塞尔曲线坐标系代数推导
final double dx = math.pow(1 - t, 2) * vwfa.dx + 2 * (1 - t) * t * controlX + math.pow(t, 2) * wernicke.dx;
final double dy = math.pow(1 - t, 2) * vwfa.dy + 2 * (1 - t) * t * controlY + math.pow(t, 2) * wernicke.dy;
currentPos = Offset(dx, dy);
pColor = Color.lerp(const Color(0xFF8B5CF6), const Color(0xFF10B981), t)!;
} else {
// 三次贝塞尔弓状束曲线推导 (段二,推演逻辑遵循公式,代码略)
// ...
}
// 渲染具有强光晕(MaskFilter.solid)的高斯粒子
final Paint particlePaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 8.0);
canvas.drawCircle(currentPos, 12.0, particlePaint); // 发光外圈
canvas.drawCircle(currentPos, 6.0, Paint()..color = pColor); // 高纯度实心内圈
}
工程思辨:在没有内置路径跟踪组件的前提下,我们通过原生的代数方程,利用多项式展开法,实现了动画游标 t 到坐标 (x, y) 的精准投射。配合极高对比度的实心白圈与色彩遮罩,营造出了“如同高能质子加速器”一般的硬核科技感反馈。
四、 结语与生命哲学沉思
当我们跳出“教与学”的传统二元论,通过这套《儿童语文认知与语义皮层映射引擎》审视教育时,每一个被认识的汉字,都不再是书本上枯燥的油墨印记,而是大脑中数以万计的神经元在皮层深处完成的一次伟大握手。当左半脑视觉词形区的光点,穿越幽深的弓状束终于点亮布洛卡区的那一刻,一个孩子不仅仅是学会了“明”、“林”或者“好”,他更是在完成一次自我神经网络的物理重塑。
将 Flutter 工程渲染与认知神经语言学相结合,其意义绝非仅仅是打造一个更炫酷的 UI。它旨在提醒我们,数字科技的力量可以如此直白且壮丽地向我们揭示生命运作的微观奇迹。这也正是跨端研发在未来探索生命科学与人文教育时,所能提供的最震撼的维度。
完整代码
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 NeuroLinguisticsApp());
}
class NeuroLinguisticsApp extends StatelessWidget {
const NeuroLinguisticsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '语言皮层映射与语义聚合引擎',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF040A0F), // 深邃碳蓝
useMaterial3: true,
fontFamily: 'Roboto',
),
home: const LinguisticsDashboard(),
);
}
}
// ==========================================
// 领域模型 (Domain Models - 语言认知与皮层网络)
// ==========================================
class HanziSynthesis {
final List<String> radicals;
final String targetChar;
final String pinyin;
final String meaning;
HanziSynthesis({
required this.radicals,
required this.targetChar,
required this.pinyin,
required this.meaning,
});
}
class BrainRegionData {
final String name;
final String func;
double activationLevel; // 0.0 - 1.0
BrainRegionData(this.name, this.func, this.activationLevel);
}
// ==========================================
// 核心状态控制面板 (Main Dashboard)
// ==========================================
class LinguisticsDashboard extends StatefulWidget {
const LinguisticsDashboard({super.key});
@override
State<LinguisticsDashboard> createState() => _LinguisticsDashboardState();
}
class _LinguisticsDashboardState extends State<LinguisticsDashboard> with TickerProviderStateMixin {
late AnimationController _neuralController;
late AnimationController _pulseController;
late AnimationController _transmissionController;
final List<HanziSynthesis> _challenges = [
HanziSynthesis(radicals: ['日', '月'], targetChar: '明', pinyin: 'míng', meaning: '光明,照亮 (Bright, Clear)'),
HanziSynthesis(radicals: ['木', '木'], targetChar: '林', pinyin: 'lín', meaning: '成片的树木 (Forest, Woods)'),
HanziSynthesis(radicals: ['女', '子'], targetChar: '好', pinyin: 'hǎo', meaning: '优良,使人满意 (Good, Excellent)'),
HanziSynthesis(radicals: ['氵', '木'], targetChar: '沐', pinyin: 'mù', meaning: '洗头发,润泽 (Bathe, Cleanse)'),
HanziSynthesis(radicals: ['门', '口'], targetChar: '问', pinyin: 'wèn', meaning: '寻取解答 (Ask, Inquire)'),
HanziSynthesis(radicals: ['火', '山'], targetChar: '灿', pinyin: 'càn', meaning: '光彩耀眼 (Brilliant, Radiant)'),
];
int _currentIndex = 0;
List<String> _selectedRadicals = [];
List<String> _availableRadicals = [];
bool _isSuccess = false;
// 脑皮层活动指标
final Map<String, BrainRegionData> _regions = {
'VWFA': BrainRegionData('视觉词形区 (VWFA)', '字形特征提取与正字法解码', 0.1),
'Wernicke': BrainRegionData('韦尼克区 (Wernicke)', '语义检索与语言理解', 0.1),
'Broca': BrainRegionData('布洛卡区 (Broca)', '语音编码与运动发音', 0.1),
};
Timer? _homeostasisTimer;
@override
void initState() {
super.initState();
_neuralController = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
)..repeat();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat(reverse: true);
_transmissionController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
); // 按需触发
_initChallenge();
// 生化状态内环境稳态 (衰减)
_homeostasisTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
bool needUpdate = false;
for (var key in _regions.keys) {
if (_regions[key]!.activationLevel > 0.1) {
_regions[key]!.activationLevel -= 0.005; // 自然衰减
needUpdate = true;
}
}
if (needUpdate) setState(() {});
});
}
void _initChallenge() {
_isSuccess = false;
_selectedRadicals.clear();
// 生成混淆偏旁
final currentChallenge = _challenges[_currentIndex];
Set<String> pool = {...currentChallenge.radicals};
final random = math.Random();
// 增加干扰项
final allRadicals = ['日', '月', '木', '女', '子', '氵', '门', '口', '火', '山', '人', '土', '心', '目'];
while (pool.length < 6) {
pool.add(allRadicals[random.nextInt(allRadicals.length)]);
}
_availableRadicals = pool.toList()..shuffle();
// 切换题目,激活视觉区
_regions['VWFA']!.activationLevel = 0.6;
}
@override
void dispose() {
_neuralController.dispose();
_pulseController.dispose();
_transmissionController.dispose();
_homeostasisTimer?.cancel();
super.dispose();
}
void _selectRadical(String radical) {
if (_isSuccess) return;
setState(() {
if (_selectedRadicals.contains(radical)) {
_selectedRadicals.remove(radical);
} else {
if (_selectedRadicals.length < 2) {
_selectedRadicals.add(radical);
// 每次点击,视觉区兴奋度提升
_regions['VWFA']!.activationLevel = math.min(1.0, _regions['VWFA']!.activationLevel + 0.3);
}
}
_checkSynthesis();
});
}
void _checkSynthesis() {
final current = _challenges[_currentIndex];
if (_selectedRadicals.length == 2) {
bool match1 = _selectedRadicals.contains(current.radicals[0]) && _selectedRadicals.contains(current.radicals[1]);
bool match2 = (current.radicals[0] == current.radicals[1]) &&
(_selectedRadicals[0] == current.radicals[0] && _selectedRadicals[1] == current.radicals[1]);
if (match1 || match2) {
// 成功聚合!
_isSuccess = true;
// 触发全脑网络放电
_regions['VWFA']!.activationLevel = 1.0;
_regions['Wernicke']!.activationLevel = 1.0;
_regions['Broca']!.activationLevel = 1.0;
_transmissionController.forward(from: 0.0);
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_currentIndex = (_currentIndex + 1) % _challenges.length;
_initChallenge();
});
}
});
} else {
// 聚合失败:仅 Wernicke 产生轻微困惑波动
_regions['Wernicke']!.activationLevel = math.min(1.0, _regions['Wernicke']!.activationLevel + 0.2);
// 1秒后清空重新选择
Future.delayed(const Duration(milliseconds: 800), () {
if (mounted && !_isSuccess) {
setState(() {
_selectedRadicals.clear();
});
}
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxWidth < 950;
if (isCompact) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: _buildBrainVisualizer(isCompact: true),
),
),
SliverToBoxAdapter(
child: Container(height: 1, color: const Color(0xFF1E293B)),
),
SliverToBoxAdapter(
child: _buildMetricsPanel(),
),
SliverToBoxAdapter(
child: _buildInteractionConsole(),
),
],
);
}
return Row(
children: [
Expanded(
flex: 4,
child: Column(
children: [
_buildMetricsPanel(),
Expanded(
child: _buildInteractionConsole(),
),
],
),
),
Container(width: 1, color: const Color(0xFF1E293B)),
Expanded(
flex: 5,
child: _buildBrainVisualizer(isCompact: false),
),
],
);
},
),
);
}
Widget _buildMetricsPanel() {
return Container(
color: const Color(0xFF030712),
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.language, color: Color(0xFF00E5FF), size: 32),
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(0xFF00E5FF).withValues(alpha: 0.1),
border: Border.all(color: const Color(0xFF00E5FF).withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'FMRI SYNC',
style: TextStyle(
color: Color(0xFF00E5FF),
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1,
),
),
),
],
),
const SizedBox(height: 32),
_buildRegionMetric('VWFA', const Color(0xFF8B5CF6)),
const SizedBox(height: 16),
_buildRegionMetric('Wernicke', const Color(0xFF10B981)),
const SizedBox(height: 16),
_buildRegionMetric('Broca', const Color(0xFFF59E0B)),
],
),
);
}
Widget _buildRegionMetric(String key, Color color) {
final region = _regions[key]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
region.name,
style: const TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
region.func,
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
),
),
],
),
),
Text(
'${(region.activationLevel * 100).toInt()}%',
style: TextStyle(
fontSize: 16,
color: color,
fontWeight: FontWeight.w900,
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: region.activationLevel,
backgroundColor: const Color(0xFF0F172A),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 6,
),
),
],
);
}
Widget _buildInteractionConsole() {
final current = _challenges[_currentIndex];
return Container(
color: const Color(0xFF070B19),
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 目标聚合舱
Container(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 32),
decoration: BoxDecoration(
color: _isSuccess ? const Color(0xFF10B981).withValues(alpha: 0.1) : const Color(0xFF1E293B).withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _isSuccess ? const Color(0xFF10B981) : const Color(0xFF334155),
width: 2,
),
boxShadow: _isSuccess ? [
BoxShadow(
color: const Color(0xFF10B981).withValues(alpha: 0.2),
blurRadius: 30,
spreadRadius: 5,
)
] : [],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildRadicalSlot(0),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.add_link, color: Colors.grey, size: 32),
),
_buildRadicalSlot(1),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(Icons.double_arrow, color: Color(0xFF00E5FF), size: 36),
),
AnimatedOpacity(
opacity: _isSuccess ? 1.0 : 0.1,
duration: const Duration(milliseconds: 500),
child: Text(
current.targetChar,
style: TextStyle(
fontSize: 64,
fontWeight: FontWeight.w900,
color: _isSuccess ? const Color(0xFF10B981) : Colors.grey,
),
),
),
],
),
if (_isSuccess) ...[
const SizedBox(height: 20),
Text(
'[ ${current.pinyin} ]',
style: const TextStyle(
color: Color(0xFFF59E0B),
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 4,
),
),
const SizedBox(height: 8),
Text(
current.meaning,
style: const TextStyle(
color: Color(0xFF10B981),
fontSize: 16,
letterSpacing: 2,
),
),
]
],
),
),
const SizedBox(height: 48),
// 备选偏旁组件池
Wrap(
spacing: 16,
runSpacing: 16,
alignment: WrapAlignment.center,
children: _availableRadicals.map((radical) {
final isSelected = _selectedRadicals.contains(radical);
return InkWell(
onTap: () => _selectRadical(radical),
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 70,
height: 70,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF00E5FF).withValues(alpha: 0.2) : const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? const Color(0xFF00E5FF) : const Color(0xFF334155),
width: isSelected ? 2 : 1,
),
),
child: Text(
radical,
style: TextStyle(
fontSize: 32,
fontWeight: isSelected ? FontWeight.w900 : FontWeight.w500,
color: isSelected ? const Color(0xFF00E5FF) : Colors.white70,
),
),
),
);
}).toList(),
),
],
),
);
}
Widget _buildRadicalSlot(int index) {
bool hasValue = _selectedRadicals.length > index;
String text = hasValue ? _selectedRadicals[index] : '?';
return Container(
width: 70,
height: 70,
alignment: Alignment.center,
decoration: BoxDecoration(
color: hasValue ? const Color(0xFF8B5CF6).withValues(alpha: 0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: hasValue ? const Color(0xFF8B5CF6) : const Color(0xFF334155),
width: 2,
),
),
child: Text(
text,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w900,
color: hasValue ? const Color(0xFF8B5CF6) : Colors.grey[700],
),
),
);
}
Widget _buildBrainVisualizer({required bool isCompact}) {
return Container(
color: const Color(0xFF020408),
child: Stack(
children: [
// Background Matrix Grid
CustomPaint(
size: Size.infinite,
painter: DataGridPainter(),
),
// Language Brain Network
AnimatedBuilder(
animation: Listenable.merge([_neuralController, _pulseController, _transmissionController]),
builder: (context, child) {
return CustomPaint(
size: Size.infinite,
painter: LanguageCorticalPainter(
vwfaActivation: _regions['VWFA']!.activationLevel,
wernickeActivation: _regions['Wernicke']!.activationLevel,
brocaActivation: _regions['Broca']!.activationLevel,
neuralProgress: _neuralController.value,
pulseProgress: _pulseController.value,
transmissionProgress: _transmissionController.value,
isTransmitting: _transmissionController.isAnimating,
),
);
},
),
// Overlay Texts
Positioned(
top: 24,
left: 24,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'语义网络连接组图谱',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
Text(
'弓状束 (Arcuate Fasciculus) 激活映射',
style: TextStyle(
color: const Color(0xFF10B981),
fontSize: 12,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
],
),
),
],
),
);
}
}
// ==========================================
// 极客物理渲染:脑功能区与神经纤维束 (弓状束) 绘制
// ==========================================
class DataGridPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF1E293B).withValues(alpha: 0.3)
..strokeWidth = 1.0;
const double spacing = 40.0;
for (double i = 0; i < size.width; i += spacing) {
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paint);
}
for (double i = 0; i < size.height; i += spacing) {
canvas.drawLine(Offset(0, i), Offset(size.width, i), paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class LanguageCorticalPainter extends CustomPainter {
final double vwfaActivation;
final double wernickeActivation;
final double brocaActivation;
final double neuralProgress;
final double pulseProgress;
final double transmissionProgress;
final bool isTransmitting;
LanguageCorticalPainter({
required this.vwfaActivation,
required this.wernickeActivation,
required this.brocaActivation,
required this.neuralProgress,
required this.pulseProgress,
required this.transmissionProgress,
required this.isTransmitting,
});
@override
void paint(Canvas canvas, Size size) {
if (size.width <= 0 || size.height <= 0) return;
// 核心脑区相对坐标设定位 (模拟左半脑侧视图)
// Broca 区:额下回 (左上方)
final Offset brocaPos = Offset(size.width * 0.3, size.height * 0.4);
// Wernicke 区:颞上回后部 (右上方偏中)
final Offset wernickePos = Offset(size.width * 0.7, size.height * 0.5);
// VWFA:枕颞区 (右下方)
final Offset vwfaPos = Offset(size.width * 0.65, size.height * 0.75);
// 绘制虚拟脑轮廓
_drawBrainSilhouette(canvas, size, brocaPos, wernickePos, vwfaPos);
// 1. 绘制神经纤维束 (Neural Tracts)
// VWFA -> Wernicke (视觉词形解码到语义)
_drawTract(canvas, vwfaPos, wernickePos, const Color(0xFF8B5CF6), const Color(0xFF10B981), vwfaActivation, 0.2);
// Wernicke -> Broca (弓状束 Arcuate Fasciculus:语义到发音)
_drawArcuateFasciculus(canvas, wernickePos, brocaPos, const Color(0xFF10B981), const Color(0xFFF59E0B), wernickeActivation);
// 2. 绘制动作电位流动 (如果聚合成功)
if (isTransmitting && transmissionProgress > 0) {
_drawTransmissionParticles(canvas, vwfaPos, wernickePos, brocaPos, transmissionProgress);
}
// 3. 绘制脑功能区核心节点
_drawRegionNode(canvas, vwfaPos, 'VWFA\n视觉词形区', const Color(0xFF8B5CF6), vwfaActivation);
_drawRegionNode(canvas, wernickePos, 'Wernicke\n语义检索', const Color(0xFF10B981), wernickeActivation);
_drawRegionNode(canvas, brocaPos, 'Broca\n运动语音', const Color(0xFFF59E0B), brocaActivation);
}
void _drawBrainSilhouette(Canvas canvas, Size size, Offset broca, Offset wernicke, Offset vwfa) {
final Paint silPaint = Paint()
..color = const Color(0xFF1E293B).withValues(alpha: 0.2)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
// 简单抽象的左脑半球侧面轮廓拟合
final Path path = Path();
path.moveTo(size.width * 0.15, size.height * 0.6); // 额叶下端
path.quadraticBezierTo(size.width * 0.1, size.height * 0.2, size.width * 0.4, size.height * 0.15); // 额叶上延到顶叶
path.quadraticBezierTo(size.width * 0.8, size.height * 0.1, size.width * 0.9, size.height * 0.6); // 顶叶到枕叶
path.quadraticBezierTo(size.width * 0.85, size.height * 0.85, size.width * 0.6, size.height * 0.9); // 枕颞底部
path.quadraticBezierTo(size.width * 0.4, size.height * 0.8, size.width * 0.35, size.height * 0.65); // 颞叶底部凹陷处
path.quadraticBezierTo(size.width * 0.2, size.height * 0.65, size.width * 0.15, size.height * 0.6); // 闭合
canvas.drawPath(path, silPaint);
}
void _drawTract(Canvas canvas, Offset p1, Offset p2, Color c1, Color c2, double activation, double curveOffset) {
final Path tractPath = Path();
tractPath.moveTo(p1.dx, p1.dy);
// 利用贝塞尔曲线画出带有弧度的神经束
final double controlX = (p1.dx + p2.dx) / 2 + (p2.dy - p1.dy) * curveOffset;
final double controlY = (p1.dy + p2.dy) / 2 - (p2.dx - p1.dx) * curveOffset;
tractPath.quadraticBezierTo(controlX, controlY, p2.dx, p2.dy);
final Paint tractPaint = Paint()
..shader = ui.Gradient.linear(p1, p2, [
c1.withValues(alpha: 0.2 + activation * 0.5),
c2.withValues(alpha: 0.2 + activation * 0.5),
])
..style = PaintingStyle.stroke
..strokeWidth = 4.0 + activation * 6.0
..strokeCap = StrokeCap.round
..maskFilter = MaskFilter.blur(BlurStyle.solid, 3.0 + activation * 5.0);
canvas.drawPath(tractPath, tractPaint);
}
void _drawArcuateFasciculus(Canvas canvas, Offset wernicke, Offset broca, Color c1, Color c2, double activation) {
// 弓状束 (Arcuate Fasciculus) 是连接Wernicke和Broca的巨大C型弯曲神经束
final Path tractPath = Path();
tractPath.moveTo(wernicke.dx, wernicke.dy);
// 向前上方绕行顶叶再下行到额叶
final double controlX1 = wernicke.dx - (wernicke.dx - broca.dx) * 0.2;
final double controlY1 = broca.dy - 120;
final double controlX2 = broca.dx + (wernicke.dx - broca.dx) * 0.2;
final double controlY2 = broca.dy - 80;
tractPath.cubicTo(controlX1, controlY1, controlX2, controlY2, broca.dx, broca.dy);
// 绘制多股纤维
for (int i = -2; i <= 2; i++) {
final Paint bundlePaint = Paint()
..shader = ui.Gradient.linear(wernicke, broca, [
c1.withValues(alpha: 0.1 + activation * 0.3),
c2.withValues(alpha: 0.1 + activation * 0.3),
])
..style = PaintingStyle.stroke
..strokeWidth = 2.0 + activation * 3.0
..maskFilter = MaskFilter.blur(BlurStyle.normal, 2.0);
canvas.save();
canvas.translate(0, i * 8.0);
canvas.drawPath(tractPath, bundlePaint);
canvas.restore();
}
}
void _drawTransmissionParticles(Canvas canvas, Offset vwfa, Offset wernicke, Offset broca, double progress) {
// 进度 0.0 ~ 0.5 : VWFA -> Wernicke
// 进度 0.5 ~ 1.0 : Wernicke -> Broca
Offset currentPos;
Color pColor;
if (progress <= 0.5) {
double t = progress * 2.0; // 归一化到 0-1
final double controlX = (vwfa.dx + wernicke.dx) / 2 + (wernicke.dy - vwfa.dy) * 0.2;
final double controlY = (vwfa.dy + wernicke.dy) / 2 - (wernicke.dx - vwfa.dx) * 0.2;
// 二次贝塞尔曲线公式求解坐标
final double dx = math.pow(1 - t, 2) * vwfa.dx + 2 * (1 - t) * t * controlX + math.pow(t, 2) * wernicke.dx;
final double dy = math.pow(1 - t, 2) * vwfa.dy + 2 * (1 - t) * t * controlY + math.pow(t, 2) * wernicke.dy;
currentPos = Offset(dx, dy);
pColor = Color.lerp(const Color(0xFF8B5CF6), const Color(0xFF10B981), t)!;
} else {
double t = (progress - 0.5) * 2.0;
final double controlX1 = wernicke.dx - (wernicke.dx - broca.dx) * 0.2;
final double controlY1 = broca.dy - 120;
final double controlX2 = broca.dx + (wernicke.dx - broca.dx) * 0.2;
final double controlY2 = broca.dy - 80;
// 三次贝塞尔曲线公式求解坐标
final double dx = math.pow(1 - t, 3) * wernicke.dx +
3 * math.pow(1 - t, 2) * t * controlX1 +
3 * (1 - t) * math.pow(t, 2) * controlX2 +
math.pow(t, 3) * broca.dx;
final double dy = math.pow(1 - t, 3) * wernicke.dy +
3 * math.pow(1 - t, 2) * t * controlY1 +
3 * (1 - t) * math.pow(t, 2) * controlY2 +
math.pow(t, 3) * broca.dy;
currentPos = Offset(dx, dy);
pColor = Color.lerp(const Color(0xFF10B981), const Color(0xFFF59E0B), t)!;
}
final Paint particlePaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 8.0);
// 绘制耀眼的信息流粒子
canvas.drawCircle(currentPos, 12.0, particlePaint);
canvas.drawCircle(currentPos, 6.0, Paint()..color = pColor);
}
void _drawRegionNode(Canvas canvas, Offset center, String label, Color color, double activation) {
// Core Node
final Paint nodePaint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawCircle(center, 15.0, nodePaint);
// Glowing Aura based on activation and pulse
final Paint auraPaint = Paint()
..color = color.withValues(alpha: 0.1 + activation * 0.4 * pulseProgress)
..style = PaintingStyle.fill
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 15.0);
canvas.drawCircle(center, 25.0 + activation * 20.0, auraPaint);
// Outer Ring
final Paint ringPaint = Paint()
..color = Colors.white.withValues(alpha: 0.5 + activation * 0.5)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
// Ring rotation animation
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(neuralProgress * math.pi * 2);
canvas.drawArc(Rect.fromCircle(center: Offset.zero, radius: 25.0), 0, math.pi * 1.5, false, ringPaint);
canvas.restore();
// Label Text
final textSpan = TextSpan(
text: label,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
);
final textPainter = TextPainter(
text: textSpan,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, Offset(center.dx - textPainter.width / 2, center.dy + 35));
}
@override
bool shouldRepaint(covariant LanguageCorticalPainter oldDelegate) {
return oldDelegate.vwfaActivation != vwfaActivation ||
oldDelegate.wernickeActivation != wernickeActivation ||
oldDelegate.brocaActivation != brocaActivation ||
oldDelegate.neuralProgress != neuralProgress ||
oldDelegate.pulseProgress != pulseProgress ||
oldDelegate.transmissionProgress != transmissionProgress ||
oldDelegate.isTransmitting != isTransmitting;
}
}
更多推荐




所有评论(0)