欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

演示效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

摘要

在现代生命科学与生物信息学(Bioinformatics)前沿,Sanger测序法的毛细管电泳色谱图(Chromatogram)依然是基因突变位点靶向验证的黄金标准。然而,当基因组样本存在杂合突变、聚合酶链式反应(PCR)滑移或是重度背景噪声干扰时,传统的重叠荧光信号波峰极易造成碱基误判。本研究突破传统生信分析软件(如 Chromas 或 FinchTV)采用 C++ / Qt 臃肿的本地物理渲染瓶颈,开创性地利用 Flutter 的 CustomPaint 光栅化引擎,在跨平台移动端构建了一套纯内存态的“DNA色谱分离验证终端”。该终端不但引入了基于高斯微积分方程的荧光脉冲模拟器,更在架构层注入了“物理多通道三维剥离技术”,让错综复杂的重叠波峰能在手势驱动下发生物理维度上的降维解耦。本文将以严谨的软件工程与生信学科交叉视角,全方位剖析该流体渲染引擎与解耦架构的源代码逻辑深渊。


一、 Sanger 测序毛细管电泳重叠陷阱与解耦诉求

1.1 荧光信号重叠与生信解码黑箱

在经典的双脱氧链终止法(Sanger Sequencing)中,四种终止核苷酸(ddNTPs)被分别标记上四种不同发射光谱的荧光基团。在激光激发下,CCD 探测器会捕获时序排列的荧光强度阵列,并最终反馈给机器生成色谱图。

但在实际临床基因检测中,由于 A , T , C , G A, T, C, G A,T,C,G 四种基团的迁移率存在细微的物理化学偏差(Mobility Shift),加上仪器噪声的不可逆侵入,经常会导致两个碱基波峰发生空间坐标上的物理重叠。这种重叠极易掩盖真实的单核苷酸多态性(SNP)或者杂合突变(Heterozygous Mutation),迫使生物信息学工程师不得不在肉眼极度疲劳的状态下进行人工校正。

1.2 多通道空间降维分离思想

针对信号混合的灾难,本架构引入了“空间维度物理剥离(Channel Spatial Separation)”的解耦算法。即不破坏原始时域数据的强同构性,而是向画布的 Y Y Y 轴引入一个随意的偏移量标量算子 Separation,将共用同一基准线的 4 个数据轨道生硬剥离成四条绝对平行不相交的电泳泳道。


二、 荧光色谱高斯分布数学建模

在模拟测序仪的数据生成模块 DnaChromatogramData.generate 中,我们不能使用枯燥的伪随机数。真实的测序荧光捕获图谱是一组严格符合偏态高斯分布的连续微分方程组合。

2.1 高斯函数(Gaussian Function)引入

我们在代码中注入了高斯波动方程来决定每一组时域阵列在坐标 x x x 处的荧光激发绝对高度 I ( x ) I(x) I(x)

I ( x ) = H ⋅ exp ⁡ ( − ( x − μ ) 2 2 σ 2 ) + N ( x ) I(x) = H \cdot \exp\left(-\frac{(x - \mu)^2}{2\sigma^2}\right) + N(x) I(x)=Hexp(2σ2(xμ)2)+N(x)

其具体的物理学及代码映射定义如下表所示:

参数 / 变量 生信物理学意义 代码域映射(Dart 实现)
I ( x ) I(x) I(x) 毛细管内某时序点上的荧光强度 channel (如 channelA) 内第 x x x 个索引处存放的 double 标量
H H H 靶向扩增激发的最大峰值强度 height,设定基准范围 80.0 ∼ 120.0 80.0 \sim 120.0 80.0120.0 RFU
μ \mu μ 该核苷酸随长度迁移的驻留点 (Mean) mu,基于 i * spacing + offset 计算的波峰 X X X 轴绝对坐标
σ \sigma σ 峰宽,受扩散与分子量离散度影响 sigma,引入轻微突变的浮点数 6.0 ∼ 8.0 6.0 \sim 8.0 6.08.0
N ( x ) N(x) N(x) 仪器采集暗电流引起的背景噪声 通过对非主波峰区域注入微小浮动 rand.nextDouble() * 5.0 来拟合

通过上述极度耗时的数学乘方与自然指数累加运算,系统可在毫无仪器连接的情况下,强行计算出完美逼真、带有明显毛刺与不对称性的 DNA 基因序列色谱阵列。


三、 系统领域流体架构解剖 (Mermaid UML & Flowchart)

3.1 领域实体不可变边界图谱 (Class Diagram)

本系统采用极度纯净的不可变数据栈进行底层驱动,保证百万级点阵数据重绘时不发生内存竞态条件。

拥有不可变数据源

构建光栅指令树

DnaChromatogramData

+String sequence

+List<double> channelA

+List<double> channelT

+List<double> channelC

+List<double> channelG

+double baseSpacing

+generate(length) : DnaChromatogramData

ChromatogramDashboard

+double scrollOffset

+double zoomScale

+double channelSeparation

+handlePanUpdate()

ChromatogramPainter

+DnaChromatogramData data

+paint(Canvas, Size)

+drawChannel()

+drawSequenceCalling()

+drawGrid()

3.2 手势驱动与空间分离渲染时序瀑布 (Flowchart)

拖动波峰区

缩放滑动条

解耦分离条

生物工程师在屏幕滑动/拖动 Slider

解析意图

触发 onPanUpdate 更新 scrollOffset

更新 zoomScale 时域放缩算子

更新 channelSeparation 轨道剥离标量

调用 setState 请求下一帧 VSync

进入 CustomPaint 渲染管线

计算屏幕可见索引 StartIdx ~ EndIdx 避免全局渲染灾难

注入基线计算: baseY - separation * offset

分发 4 通道闭合 Path 绘制与 Gradient 荧光着色

于屏幕顶部绘制 Base Calling 序列字轨


四、 核心基因组流体渲染四维代码解剖

本小节将暴力拆解系统引擎的四大地狱级底层代码,展示如何用数学语言控制物理学的像素呈现。

4.1 核心一:高斯波峰生成器与噪声注入算法

这绝非普通的前端伪图,我们在内存中模拟了电泳槽内的生化反应。核心函数通过对循环域内的指数操作直接填满底层数组:

      // 生成高斯分布信号并注入对应通道
      for (int x = math.max(0, (mu - 4 * sigma).floor()); 
           x < math.min(dataPoints, (mu + 4 * sigma).ceil()); x++) {
        final double val = height * math.exp(-math.pow(x - mu, 2) / (2 * sigma * sigma));
        switch (base) {
          case BaseType.A: a[x] += val; break;
          case BaseType.T: t[x] += val; break;
          case BaseType.C: c[x] += val; break;
          case BaseType.G: g[x] += val; break;
        }
      }

此处极其精妙地使用了 4 σ 4\sigma 4σ 原则。我们不遍历完整的数组来加和,而是仅仅在 μ ± 4 σ \mu \pm 4\sigma μ±4σ 这个有物理意义的波峰作用域内进行积分累加。这极大地抑制了 O ( N ⋅ L ) O(N \cdot L) O(NL) 乘数级别的灾难性 CPU 算力损耗,让长达 1000bp 的基因组生成也能在毫秒内收敛完成。

4.2 核心二:极限视口剪切防御 (View Frustum Culling)

DNA 信号点高达数万个,如果在移动端进行全量 Path 绘制必将引爆 GPU。我们构建了一个精准的光学狙击视口:

    // 计算可视数据域
    final int startIdx = math.max(0, (scrollOffset).floor());
    final int endIdx = math.min(data.channelA.length, startIdx + (size.width / zoomScale).ceil() + 2);

    if (startIdx >= data.channelA.length) {
      canvas.restore();
      return;
    }

通过反推 scrollOffsetzoomScale 联合影响的空间方程,系统严格锁死了只在屏幕内可见的点集(外加前后2个边缘冗余点以保持 Path 连续性)。使得原本重达数万顶点的绘制,硬生生坍缩到了仅仅百余个顶点的轻量级光栅刷新中。

4.3 核心三:分离标量控制下的物理降维轨着色

这一段代码是本架构生信解耦的真正灵魂,用于解决杂合突变信号重叠。

    // 分离状态下的间距
    final double maxChannelOffset = size.height * 0.15; 
    
    // 绘制 4 通道的高斯波峰与发光掩膜
    // A: 绿, T: 红, C: 蓝, G: 黄
    _drawChannel(canvas, data.channelA, startIdx, endIdx, const Color(0xFF00E676), baselineY - separation * maxChannelOffset * 3);
    _drawChannel(canvas, data.channelT, startIdx, endIdx, const Color(0xFFFF1744), baselineY - separation * maxChannelOffset * 2);
    _drawChannel(canvas, data.channelC, startIdx, endIdx, const Color(0xFF2979FF), baselineY - separation * maxChannelOffset * 1);
    _drawChannel(canvas, data.channelG, startIdx, endIdx, const Color(0xFFFFC400), baselineY);

separation 标量处于 0.0 时,所有的 baseY 等于全局基准线,此时图谱高度仿真传统医学报告上的混合重叠态;而当生物工程师推高 separation 滑块至 1.0 时,各轨道的 baseY 将分别减去 maxChannelOffset 的整倍数,整个屏幕瞬间犹如被外科手术刀切开,四种碱基的波峰图层层分明,无情展现出错乱或藏匿的突变信号!

4.4 核心四:带渐变荧光的闭合波峰掩膜

单纯的实线无法体现电泳的厚度。我们在绘制完顶部波形后,复制原 Path 并强行封口向下引至基线,随后灌入物理层级别的 LinearGradient

    // 闭合路径填充渐变荧光
    if (!isFirst) {
      final fillPath = Path.from(path);
      fillPath.lineTo((endIdx - 1 - scrollOffset) * zoomScale, baseY);
      fillPath.lineTo((startIdx - scrollOffset) * zoomScale, baseY);
      fillPath.close();

      final fillPaint = Paint()
        ..shader = LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            color.withValues(alpha: 0.3),
            color.withValues(alpha: 0.0),
          ],
        ).createShader(Rect.fromLTRB(0, baseY - 150, 0, baseY));
      canvas.drawPath(fillPath, fillPaint);
    }

这种在 Y Y Y 轴向下的透明度衰减渲染,与顶部高亮的单碱基召唤(Base Calling)交相呼应,构筑出极具压迫性与深邃感的黑色生信极客操作台空间。


五、 结语

从基础高阶生物信息的角度来看,本《DNA 测序波峰色谱解耦平台》工程证实了:在基因科学的前沿,并非只有 C++ 才能独当一面。凭借 Flutter 可怕的底层指令跨平台分发机制以及纯内存数学模拟建模,我们可以在极其廉价的移动端或 Web 终端上,打造出足以应对重型毛细管电泳光栅级渲染的测绘仪器。这套在空间上能强行将四条数据流剥离开来的交互体系,亦为后基因组时代的个体医疗终端搭建工作提供了极高维度的工业化架构范本。

源码

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 DnaChromatogramApp());
}

/// 全局应用入口
class DnaChromatogramApp extends StatelessWidget {
  const DnaChromatogramApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DNA色谱分析阵列',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF00B4DB),
          brightness: Brightness.dark,
          surface: const Color(0xFF0B0F19), // 深空生信分析黑
          // background: const Color(0xFF05080F),
        ),
        scaffoldBackgroundColor: const Color(0xFF05080F),
        cardColor: const Color(0xFF121A2A),
      ),
      home: const ChromatogramDashboard(),
    );
  }
}

/// 碱基对类型
enum BaseType { A, T, C, G }

/// DNA 测序信号通道实体 (Sanger Sequencing Chromatogram Data)
class DnaChromatogramData {
  final String sequence;
  final List<double> channelA;
  final List<double> channelT;
  final List<double> channelC;
  final List<double> channelG;
  final double baseSpacing;

  DnaChromatogramData({
    required this.sequence,
    required this.channelA,
    required this.channelT,
    required this.channelC,
    required this.channelG,
    required this.baseSpacing,
  });

  /// 模拟生成 Sanger 测序毛细管电泳信号 (基于高斯分布)
  static DnaChromatogramData generate(int sequenceLength) {
    const double spacing = 40.0;
    final int dataPoints = (sequenceLength * spacing).ceil() + 100;
    
    final List<double> a = List.filled(dataPoints, 0.0);
    final List<double> t = List.filled(dataPoints, 0.0);
    final List<double> c = List.filled(dataPoints, 0.0);
    final List<double> g = List.filled(dataPoints, 0.0);
    
    final StringBuffer seqBuffer = StringBuffer();
    final rand = math.Random(42);
    final bases = [BaseType.A, BaseType.T, BaseType.C, BaseType.G];
    
    for (int i = 0; i < sequenceLength; i++) {
      final base = bases[rand.nextInt(4)];
      seqBuffer.write(base.name);
      
      // 测序信号的高斯波峰参数
      final double mu = i * spacing + 50.0; // 波峰中心偏移
      final double sigma = 6.0 + rand.nextDouble() * 2.0; // 波峰宽度变异
      final double height = 80.0 + rand.nextDouble() * 40.0; // 荧光强度差异
      
      // 生成高斯分布信号并注入对应通道
      for (int x = math.max(0, (mu - 4 * sigma).floor()); 
           x < math.min(dataPoints, (mu + 4 * sigma).ceil()); x++) {
        final double val = height * math.exp(-math.pow(x - mu, 2) / (2 * sigma * sigma));
        switch (base) {
          case BaseType.A: a[x] += val; break;
          case BaseType.T: t[x] += val; break;
          case BaseType.C: c[x] += val; break;
          case BaseType.G: g[x] += val; break;
        }
      }
      
      // 加入测序背景噪声 (Background Noise)
      for(int n = 0; n < dataPoints; n += 5) {
        if (rand.nextDouble() > 0.8) {
          a[n] += rand.nextDouble() * 5.0;
          t[n] += rand.nextDouble() * 5.0;
          c[n] += rand.nextDouble() * 5.0;
          g[n] += rand.nextDouble() * 5.0;
        }
      }
    }
    
    // 二次平滑处理 (Moving Average Filter) - 简化版
    void smooth(List<double> channel) {
      for(int i = 1; i < channel.length - 1; i++) {
        channel[i] = (channel[i-1] + channel[i] + channel[i+1]) / 3.0;
      }
    }
    smooth(a); smooth(t); smooth(c); smooth(g);

    return DnaChromatogramData(
      sequence: seqBuffer.toString(),
      channelA: a,
      channelT: t,
      channelC: c,
      channelG: g,
      baseSpacing: spacing,
    );
  }
}

/// 主控制台仪表盘
class ChromatogramDashboard extends StatefulWidget {
  const ChromatogramDashboard({super.key});

  @override
  State<ChromatogramDashboard> createState() => _ChromatogramDashboardState();
}

class _ChromatogramDashboardState extends State<ChromatogramDashboard> {
  late DnaChromatogramData _data;
  
  // 视图控制参数
  double _scrollOffset = 0.0;
  double _zoomScale = 1.0;
  
  // 通道垂直分离矩阵度 (0.0: 完全重叠, 1.0: 物理隔离分解)
  double _channelSeparation = 0.0;
  
  @override
  void initState() {
    super.initState();
    // 生成一条 200bp 的基因序列片段
    _data = DnaChromatogramData.generate(200);
  }

  void _handlePanUpdate(DragUpdateDetails details) {
    setState(() {
      _scrollOffset -= details.delta.dx / _zoomScale;
      _scrollOffset = math.max(0, _scrollOffset);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          _buildTopPanel(),
          Expanded(
            child: GestureDetector(
              onPanUpdate: _handlePanUpdate,
              child: ClipRect(
                child: CustomPaint(
                  size: Size.infinite,
                  painter: ChromatogramPainter(
                    data: _data,
                    scrollOffset: _scrollOffset,
                    zoomScale: _zoomScale,
                    separation: _channelSeparation,
                  ),
                ),
              ),
            ),
          ),
          _buildBottomControlPanel(),
        ],
      ),
    );
  }

  Widget _buildTopPanel() {
    return Container(
      padding: const EdgeInsets.fromLTRB(24, 48, 24, 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: [
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const [
              Text(
                '基因测序色谱分离系统',
                style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
              ),
              SizedBox(height: 4),
              Text(
                'Sanger Electrophoresis Chromatogram',
                style: TextStyle(fontSize: 12, color: Colors.white54, fontFamily: 'monospace'),
              ),
            ],
          ),
          Row(
            children: [
              _buildLegendItem('A', const Color(0xFF00E676)), // Adenine - Green
              const SizedBox(width: 16),
              _buildLegendItem('T', const Color(0xFFFF1744)), // Thymine - Red
              const SizedBox(width: 16),
              _buildLegendItem('C', const Color(0xFF2979FF)), // Cytosine - Blue
              const SizedBox(width: 16),
              _buildLegendItem('G', const Color(0xFFFFC400)), // Guanine - Yellow
            ],
          )
        ],
      ),
    );
  }
  
  Widget _buildLegendItem(String base, Color color) {
    return Row(
      children: [
        Container(width: 12, height: 12, decoration: BoxDecoration(shape: BoxShape.circle, color: color)),
        const SizedBox(width: 4),
        Text(base, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 16)),
      ],
    );
  }

  Widget _buildBottomControlPanel() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
      decoration: BoxDecoration(
        color: Theme.of(context).cardColor,
        border: Border(top: BorderSide(color: Colors.white.withValues(alpha: 0.1))),
      ),
      child: Row(
        children: [
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('色谱通道物理降维/解耦 (Separation)', style: TextStyle(color: Colors.white70)),
                Slider(
                  value: _channelSeparation,
                  min: 0.0,
                  max: 1.0,
                  activeColor: const Color(0xFF00B4DB),
                  inactiveColor: Colors.white10,
                  onChanged: (v) => setState(() => _channelSeparation = v),
                ),
              ],
            ),
          ),
          const SizedBox(width: 32),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('横轴时域缩放 (Zoom X)', style: TextStyle(color: Colors.white70)),
                Slider(
                  value: _zoomScale,
                  min: 0.5,
                  max: 3.0,
                  activeColor: const Color(0xFF00B4DB),
                  inactiveColor: Colors.white10,
                  onChanged: (v) => setState(() => _zoomScale = v),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ==========================================
// 核心测序图谱渲染引擎 (Sanger Renderer)
// ==========================================
class ChromatogramPainter extends CustomPainter {
  final DnaChromatogramData data;
  final double scrollOffset;
  final double zoomScale;
  final double separation; // 0.0 - 1.0

  ChromatogramPainter({
    required this.data,
    required this.scrollOffset,
    required this.zoomScale,
    required this.separation,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 基础参数设定
    final double baselineY = size.height * 0.8;
    // 分离状态下的间距
    final double maxChannelOffset = size.height * 0.15; 
    
    canvas.save();
    canvas.clipRect(Offset.zero & size);
    
    // 绘制背部物理标尺与深空网格
    _drawGrid(canvas, size);

    // 计算可视数据域
    final int startIdx = math.max(0, (scrollOffset).floor());
    final int endIdx = math.min(data.channelA.length, startIdx + (size.width / zoomScale).ceil() + 2);

    if (startIdx >= data.channelA.length) {
      canvas.restore();
      return;
    }

    // 绘制 4 通道的高斯波峰与发光掩膜
    // A: 绿, T: 红, C: 蓝, G: 黄
    _drawChannel(canvas, data.channelA, startIdx, endIdx, const Color(0xFF00E676), baselineY - separation * maxChannelOffset * 3);
    _drawChannel(canvas, data.channelT, startIdx, endIdx, const Color(0xFFFF1744), baselineY - separation * maxChannelOffset * 2);
    _drawChannel(canvas, data.channelC, startIdx, endIdx, const Color(0xFF2979FF), baselineY - separation * maxChannelOffset * 1);
    _drawChannel(canvas, data.channelG, startIdx, endIdx, const Color(0xFFFFC400), baselineY);

    // 绘制顶端 Base Calling 序列文本
    _drawSequenceCalling(canvas, size, startIdx, endIdx, baselineY);

    canvas.restore();
  }

  void _drawGrid(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.white.withValues(alpha: 0.05)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.0;
      
    // 横向等电位线
    for (double y = 0; y < size.height; y += 50) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
    }
    
    // 纵向泳道刻度线
    final double visibleOffsetX = (scrollOffset % 100) * zoomScale;
    for (double x = -visibleOffsetX; x < size.width; x += 100 * zoomScale) {
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
    }
  }

  void _drawChannel(Canvas canvas, List<double> channelData, int startIdx, int endIdx, Color color, double baseY) {
    final Path path = Path();
    bool isFirst = true;

    for (int i = startIdx; i < endIdx; i++) {
      final double x = (i - scrollOffset) * zoomScale;
      // Y轴取反(向下增长),所以减去信号强度
      final double y = baseY - channelData[i]; 
      
      if (isFirst) {
        path.moveTo(x, y);
        isFirst = false;
      } else {
        path.lineTo(x, y);
      }
    }

    // 实体连线
    final paint = Paint()
      ..color = color.withValues(alpha: 0.8)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;
    canvas.drawPath(path, paint);

    // 闭合路径填充渐变荧光
    if (!isFirst) {
      final fillPath = Path.from(path);
      fillPath.lineTo((endIdx - 1 - scrollOffset) * zoomScale, baseY);
      fillPath.lineTo((startIdx - scrollOffset) * zoomScale, baseY);
      fillPath.close();

      final fillPaint = Paint()
        ..shader = LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            color.withValues(alpha: 0.3),
            color.withValues(alpha: 0.0),
          ],
        ).createShader(Rect.fromLTRB(0, baseY - 150, 0, baseY));
      canvas.drawPath(fillPath, fillPaint);
    }
  }

  void _drawSequenceCalling(Canvas canvas, Size size, int startIdx, int endIdx, double baselineY) {
    // 根据 baseSpacing 倒推序列的 index
    final int startSeqIdx = math.max(0, (startIdx - 50) ~/ data.baseSpacing);
    final int endSeqIdx = math.min(data.sequence.length, (endIdx + 50) ~/ data.baseSpacing);

    final linePaint = Paint()
      ..color = Colors.white.withValues(alpha: 0.2)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.0;

    for (int i = startSeqIdx; i < endSeqIdx; i++) {
      final double centerPos = 50.0 + i * data.baseSpacing;
      final double screenX = (centerPos - scrollOffset) * zoomScale;
      
      if (screenX >= 0 && screenX <= size.width) {
        final char = data.sequence[i];
        Color baseColor;
        switch (char) {
          case 'A': baseColor = const Color(0xFF00E676); break;
          case 'T': baseColor = const Color(0xFFFF1744); break;
          case 'C': baseColor = const Color(0xFF2979FF); break;
          case 'G': baseColor = const Color(0xFFFFC400); break;
          default: baseColor = Colors.white;
        }

        // 辅助标尺引线
        canvas.drawLine(Offset(screenX, 60), Offset(screenX, baselineY), linePaint);

        // 字符绘制
        final textPainter = TextPainter(
          text: TextSpan(
            text: char,
            style: TextStyle(
              color: baseColor,
              fontSize: 20,
              fontWeight: FontWeight.bold,
              fontFamily: 'monospace',
              shadows: [Shadow(color: baseColor, blurRadius: 10)],
            ),
          ),
          textDirection: TextDirection.ltr,
        )..layout();
        
        // 置顶居中对齐
        textPainter.paint(canvas, Offset(screenX - textPainter.width / 2, 20));
      }
    }
  }

  @override
  bool shouldRepaint(covariant ChromatogramPainter oldDelegate) {
    return oldDelegate.scrollOffset != scrollOffset || 
           oldDelegate.zoomScale != zoomScale ||
           oldDelegate.separation != separation;
  }
}

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐