Flutter for OpenHarmony3D DNA 螺旋可视化:用 Canvas 构建沉浸式分子模型

Flutter for OpenHarmony3D DNA 螺旋可视化:用 Canvas 构建沉浸式分子模型

🌐 加入社区

欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持:

👉 开源鸿蒙跨平台开发者社区

在科学与艺术的交汇处,DNA 双螺旋结构 始终是生命最优雅的象征。如今,借助 Flutter 强大的自定义绘制能力,我们无需依赖

WebGL 或复杂 3D 引擎,即可在移动端构建一个可交互、带景深、具真实感的 3D DNA
分子模型
。本文将深入剖析一段完整代码,揭示如何通过纯 2D Canvas 实现伪 3D

效果、粒子背景、动态旋转与深度排序,打造一款兼具教育意义与视觉震撼的生物可视化应用。


完整效果展示

一、架构全景:从 3D 抽象到 2D 渲染

整个系统由三层构成:

层级 组件 职责
数据层 Point3D, BasePair 定义三维空间中的碱基对坐标
投影层 _projectPoint() 将 3D 点转换为带景深缩放的 2D 坐标
渲染层 DNAHelixPainter 绘制骨架、碱基、氢键与光效

💡 核心思想:用数学模拟 3D,用 Canvas 表现 3D ------ 这正是"伪 3D"(2.5D)渲染的精髓。


二、3D 到 2D 的魔法:透视投影与倾斜视角

1. 坐标系与参数设定

dart 复制代码
static const double _helixRadius = 80;      // 螺旋半径
static const double _helixHeight = 600;     // 总高度
static const int _basePairsCount = 40;      // 碱基对数量
static const double _basePairSpacing = _helixHeight / _basePairsCount;
  • 每 6 对碱基完成一次 360° 旋转(angle = i * π / 6),符合真实 DNA 结构;
  • 两条链相位相差 180°(x2 = -x1, y2 = -y1),形成反向平行双链。

2. 倾斜视角实现

dart 复制代码
// 绕 X 轴旋转 tiltAngle 弧度
final rotatedY = point.y * cos(tiltAngle) - point.z * sin(tiltAngle);
final rotatedZ = point.y * sin(tiltAngle) + point.z * cos(tiltAngle);
  • 通过绕 X 轴旋转,将原本垂直的螺旋"倾斜"观察,增强立体感;
  • 用户可切换 tiltAngle = 0.3(≈17°)或 0.5(≈29°)两种视角。

3. 透视投影公式

dart 复制代码
final distance = _cameraDistance + rotatedZ;
final scale = _focalLength / distance;
final x = center.dx + rotatedX * scale;
final y = center.dy + rotatedY * scale;
  • 近大远小scale 随 Z 坐标(深度)变化;
  • 相机模型_focalLength = 800, _cameraDistance = 400 模拟人眼视角;
  • 中心偏移:所有点以屏幕中心为原点绘制。

✨ 此即单点透视投影(One-point perspective),是 2.5D 渲染的基石。


三、深度排序:解决遮挡问题的关键

在 3D 渲染中,近物遮挡远物 是基本法则。但在 2D Canvas 中,绘制顺序决定遮挡关系。因此必须按深度排序后绘制

dart 复制代码
List<_SortedBasePair> _sortByDepth(...) {
  for (int i = 0; i < basePairs.length; i++) {
    final avgDepth = (proj1.scale + proj2.scale) / 2; // scale 越大越近
    sorted.add(_SortedBasePair(..., avgDepth: avgDepth));
  }
  sorted.sort((a, b) => a.avgDepth.compareTo(b.avgDepth)); // 从小到大(远→近)
  return sorted;
}
  • 深度代理 :用 scale 值代替真实 Z 坐标(因已投影);
  • 平均深度:取碱基对两点的平均,避免单点偏差;
  • 绘制顺序:先画远的,再画近的,确保近物覆盖远物。

⚠️ 若不排序,远处的碱基会错误地覆盖近处结构,破坏 3D 感。


四、视觉表现力:从几何到光影的艺术

1. 双链骨架

dart 复制代码
// 蓝色链(腺嘌呤侧)
canvas.drawLine(proj1, proj2, 
  Paint()..color = Colors.blue.withValues(alpha: 0.4)
         ..strokeWidth = 4 * avgScale
);

// 粉色链(胸腺嘧啶侧)
// ... 同理
  • 渐变粗细 :线宽随 scale 变化,近粗远细;
  • 半透明处理alpha: 0.4 避免遮挡内部结构;
  • 圆角端点StrokeCap.round 使线条更柔和。

2. 碱基球体(四层绘制)

dart 复制代码
// 1. 发光光晕(大半径+高斯模糊)
canvas.drawCircle(glowRadius, baseColor@glowAlpha + blur(12));

// 2. 主体球体
canvas.drawCircle(radius, baseColor@alpha);

// 3. 径向高光(左上偏移)
canvas.drawCircle(offset, white@0.6*alpha, radialGradient);

// 4. 外圈描边
canvas.drawCircle(radius+3, white@0.2*alpha, stroke);
  • 材质感:高光模拟光源(假设来自左上);
  • 呼吸感 :光晕随深度变化(glowAlpha = 0.15 + depth*0.4);
  • 色彩编码:蓝色 = 腺嘌呤(A),粉色 = 胸腺嘧啶(T)。

3. 氢键连接

dart 复制代码
canvas.drawLine(point1, point2,
  Paint()..color = Colors.cyan.withValues(alpha: alpha*0.6)
         ..strokeWidth = 2.5 * avgScale
);
  • 动态透明:氢键随深度淡入淡出;
  • 比例协调:线宽略细于骨架,突出主次关系。

五、环境营造:星空背景与交互控制

1. 动态粒子背景

dart 复制代码
class _BackgroundParticles extends StatefulWidget {
  // 生成50个随机位置/大小/速度的粒子
  // 在 CustomPainter 中垂直滚动(y += animationValue * speed)
}
  • 宇宙隐喻:微小粒子象征浩瀚基因宇宙中的信息单元;
  • 视差效果:不同速度制造层次感;
  • 低干扰设计alpha: 0.1~0.4 确保不喧宾夺主。

2. 三重交互控制

按钮 功能 实现
速度 切换 0.5x / 1x / 2x 修改 AnimationController.duration
视角 切换倾斜角度 更新 tiltAngle 并重绘
重置 重启动画 controller.reset().repeat()

🎮 所有操作均通过 setState() 触发 AnimatedBuilder 重绘,保证流畅性。


六、UI/UX 设计:科学与美学的融合

1. 深空主题

  • 背景色:0xFF0A0E1A(深蓝黑),模拟宇宙背景;
  • 强调色:青色(Colors.cyan)代表科技与生命;
  • 卡片:蓝紫渐变 + 青色边框,呼应 DNA 色彩体系。

2. 信息分层

  • 顶部 AppBar:图标 + 标题,简洁专业;
  • 中部 InfoCard:实时显示倾斜角、速度、碱基数;
  • 底部 Legend:图例说明颜色含义与结构知识;
  • 居中主体:DNA 模型占据视觉焦点。

3. 教育价值

"双螺旋结构展示了DNA分子的三维形态,每旋转360°包含6个碱基对。"

------ 应用内嵌的简明说明,让非专业用户也能理解核心概念。


七、性能与扩展性

1. 高效重绘

  • 局部更新shouldRepaint 仅当 rotationtiltAngle 变化时重绘;
  • 帧率稳定AnimationController vsync 同步屏幕刷新;
  • 对象复用:碱基对在每帧重新计算,但无内存分配高峰。

2. 未来扩展方向

方向 实现思路
碱基序列 支持输入 ATCG 序列,动态着色
交互探索 点击碱基显示名称/配对规则
AR 查看 通过 ARCore/ARKit 投射到现实桌面
RNA 对比 添加单链 RNA 模式
性能模式 降低碱基对数量适配低端设备

结语:代码即生命之诗

这个 DNA 螺旋可视化器远不止是一个技术演示------它是对生命密码的数字化礼赞 。每一行数学公式都在还原沃森与克里克发现的优雅结构,每一次旋转都在邀请用户探索基因的奥秘。当你调整视角,看着青色氢键在深空中闪烁,那一刻,你看到的不仅是代码,更是40 亿年进化写就的生命诗篇

完整代码展示

bash 复制代码
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';

void main() {
  runApp(const DNAHelixApp());
}

class DNAHelixApp extends StatelessWidget {
  const DNAHelixApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DNA螺旋可视化',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: const Color(0xFF0A0E1A),
        appBarTheme: const AppBarTheme(
          elevation: 0,
          backgroundColor: Color(0xFF0A0E1A),
          centerTitle: true,
          titleTextStyle: TextStyle(
            fontSize: 22,
            fontWeight: FontWeight.w600,
            letterSpacing: 1.2,
          ),
        ),
      ),
      home: const DNAHelixScreen(),
    );
  }
}

class DNAHelixScreen extends StatefulWidget {
  const DNAHelixScreen({super.key});

  @override
  State<DNAHelixScreen> createState() => _DNAHelixScreenState();
}

class _DNAHelixScreenState extends State<DNAHelixScreen>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  double _rotation = 0.0;
  double _tiltAngle = 0.3;
  double _rotationSpeed = 1.0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 20),
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.biotech, size: 28),
            const SizedBox(width: 12),
            const Text('3D DNA螺旋'),
          ],
        ),
        actions: [
          _buildControlButton(Icons.speed, '速度', () {
            setState(() {
              _rotationSpeed = _rotationSpeed == 1.0
                  ? 2.0
                  : _rotationSpeed == 2.0
                      ? 0.5
                      : 1.0;
              _controller.duration =
                  Duration(milliseconds: (20000 / _rotationSpeed).round());
            });
          }),
          _buildControlButton(Icons.view_in_ar, '视角', () {
            setState(() {
              _tiltAngle = _tiltAngle == 0.3 ? 0.5 : 0.3;
            });
          }),
          _buildControlButton(Icons.refresh, '重置', () {
            setState(() {
              _controller.reset();
              _controller.repeat();
            });
          }),
          const SizedBox(width: 8),
        ],
      ),
      body: Stack(
        children: [
          // 背景粒子效果
          const _BackgroundParticles(),
          // 主内容
          SafeArea(
            child: Column(
              children: [
                _buildInfoCard(),
                Expanded(
                  child: AnimatedBuilder(
                    animation: _controller,
                    builder: (context, child) {
                      _rotation = _controller.value * 2 * pi;
                      return Center(
                        child: CustomPaint(
                          size: Size.infinite,
                          painter: DNAHelixPainter(
                            rotation: _rotation,
                            tiltAngle: _tiltAngle,
                          ),
                        ),
                      );
                    },
                  ),
                ),
                _buildLegend(),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildControlButton(
      IconData icon, String tooltip, VoidCallback onPressed) {
    return Tooltip(
      message: tooltip,
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 4),
        child: Material(
          color: Colors.white.withValues(alpha: 0.1),
          borderRadius: BorderRadius.circular(8),
          child: InkWell(
            onTap: onPressed,
            borderRadius: BorderRadius.circular(8),
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Icon(
                icon,
                size: 22,
                color: Colors.cyan.withValues(alpha: 0.9),
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildInfoCard() {
    return Container(
      margin: const EdgeInsets.all(16),
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [
            Colors.blue.withValues(alpha: 0.15),
            Colors.purple.withValues(alpha: 0.1),
          ],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: Colors.cyan.withValues(alpha: 0.3),
          width: 1,
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.cyan.withValues(alpha: 0.1),
            blurRadius: 20,
            spreadRadius: 5,
          ),
        ],
      ),
      child: Row(
        children: [
          _buildInfoItem(
            Icons.view_in_ar,
            '倾斜角度',
            '${(_tiltAngle * 180 / pi).toStringAsFixed(1)}°',
            Colors.cyan,
          ),
          const SizedBox(width: 24),
          _buildInfoItem(
            Icons.speed,
            '旋转速度',
            '${_rotationSpeed}x',
            Colors.green,
          ),
          const SizedBox(width: 24),
          _buildInfoItem(
            Icons.timeline,
            '碱基对数',
            '40',
            Colors.purple,
          ),
        ],
      ),
    );
  }

  Widget _buildInfoItem(
    IconData icon,
    String label,
    String value,
    Color color,
  ) {
    return Expanded(
      child: Column(
        children: [
          Icon(icon, color: color, size: 24),
          const SizedBox(height: 8),
          Text(
            label,
            style: TextStyle(
              color: Colors.grey.withValues(alpha: 0.8),
              fontSize: 12,
            ),
          ),
          const SizedBox(height: 4),
          Text(
            value,
            style: TextStyle(
              color: color,
              fontSize: 20,
              fontWeight: FontWeight.bold,
              letterSpacing: 1,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildLegend() {
    return Container(
      margin: const EdgeInsets.all(16),
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white.withValues(alpha: 0.05),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: Colors.white.withValues(alpha: 0.1),
          width: 1,
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '结构说明',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.w600,
              letterSpacing: 0.5,
            ),
          ),
          const SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildLegendItem(
                Colors.blue,
                '腺嘌呤 (A)',
                '蓝色链',
              ),
              _buildLegendItem(
                Colors.pink,
                '胸腺嘧啶 (T)',
                '粉色链',
              ),
              _buildLegendItem(
                Colors.cyan,
                '氢键',
                '碱基对连接',
              ),
            ],
          ),
          const SizedBox(height: 12),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: Text(
              '双螺旋结构展示了DNA分子的三维形态,每旋转360°包含6个碱基对。',
              style: TextStyle(
                color: Colors.grey.withValues(alpha: 0.6),
                fontSize: 12,
                height: 1.5,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildLegendItem(
    Color color,
    String title,
    String subtitle,
  ) {
    return Column(
      children: [
        Container(
          width: 20,
          height: 20,
          decoration: BoxDecoration(
            color: color,
            shape: BoxShape.circle,
            boxShadow: [
              BoxShadow(
                color: color.withValues(alpha: 0.5),
                blurRadius: 8,
                spreadRadius: 2,
              ),
            ],
          ),
        ),
        const SizedBox(height: 8),
        Text(
          title,
          style: TextStyle(
            color: color,
            fontSize: 12,
            fontWeight: FontWeight.w600,
          ),
        ),
        const SizedBox(height: 4),
        Text(
          subtitle,
          style: TextStyle(
            color: Colors.grey.withValues(alpha: 0.7),
            fontSize: 11,
          ),
        ),
      ],
    );
  }
}

class _BackgroundParticles extends StatefulWidget {
  const _BackgroundParticles();

  @override
  State<_BackgroundParticles> createState() => _BackgroundParticlesState();
}

class _BackgroundParticlesState extends State<_BackgroundParticles>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<_Particle> _particles = [];

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 30),
    )..repeat();
    _generateParticles();
  }

  void _generateParticles() {
    final random = Random();
    for (int i = 0; i < 50; i++) {
      _particles.add(_Particle(
        x: random.nextDouble(),
        y: random.nextDouble(),
        size: random.nextDouble() * 3 + 1,
        speed: random.nextDouble() * 0.001 + 0.0005,
        opacity: random.nextDouble() * 0.3 + 0.1,
      ));
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return CustomPaint(
          size: Size.infinite,
          painter: _ParticlesPainter(_particles, _controller.value),
        );
      },
    );
  }
}

class _Particle {
  final double x;
  final double y;
  final double size;
  final double speed;
  final double opacity;

  _Particle({
    required this.x,
    required this.y,
    required this.size,
    required this.speed,
    required this.opacity,
  });
}

class _ParticlesPainter extends CustomPainter {
  final List<_Particle> particles;
  final double animationValue;

  _ParticlesPainter(this.particles, this.animationValue);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..style = PaintingStyle.fill;

    for (final particle in particles) {
      final y = (particle.y + animationValue * particle.speed) % 1.0;
      final x = particle.x;

      paint.color = Colors.cyan.withValues(alpha: particle.opacity);
      canvas.drawCircle(
        Offset(x * size.width, y * size.height),
        particle.size,
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant _ParticlesPainter oldDelegate) {
    return oldDelegate.animationValue != animationValue;
  }
}

class DNAHelixPainter extends CustomPainter {
  final double rotation;
  final double tiltAngle;

  DNAHelixPainter({
    required this.rotation,
    required this.tiltAngle,
  });

  static const double _focalLength = 800;
  static const double _cameraDistance = 400;
  static const int _basePairsCount = 40;
  static const double _helixRadius = 80;
  static const double _helixHeight = 600;
  static const double _basePairSpacing = _helixHeight / _basePairsCount;

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);

    // 绘制背景光晕
    _drawBackgroundGlow(canvas, center);

    final basePairs = _generateBasePairs();
    final projectedPoints = _projectPoints(basePairs, center, size);
    final sortedBasePairs = _sortByDepth(basePairs, projectedPoints);

    _drawHelix(canvas, sortedBasePairs, projectedPoints);
  }

  void _drawBackgroundGlow(Canvas canvas, Offset center) {
    final glowPaint = Paint()
      ..shader = ui.Gradient.radial(
        center,
        200,
        [Colors.cyan.withValues(alpha: 0.1), Colors.transparent],
        [0.0, 1.0],
      )
      ..style = PaintingStyle.fill;

    canvas.drawCircle(center, 200, glowPaint);
  }

  List<BasePair> _generateBasePairs() {
    final basePairs = <BasePair>[];

    for (int i = 0; i < _basePairsCount; i++) {
      final t = i * _basePairSpacing;
      final angle = rotation + (i * pi / 6);

      final x1 = _helixRadius * cos(angle);
      final y1 = _helixRadius * sin(angle);
      final z1 = t - _helixHeight / 2;

      final x2 = -_helixRadius * cos(angle);
      final y2 = -_helixRadius * sin(angle);
      final z2 = t - _helixHeight / 2;

      basePairs.add(BasePair(
        point1: Point3D(x1, y1, z1),
        point2: Point3D(x2, y2, z2),
        pairIndex: i,
      ));
    }

    return basePairs;
  }

  List<ProjectedPoint> _projectPoints(
    List<BasePair> basePairs,
    Offset center,
    Size size,
  ) {
    final projectedPoints = <ProjectedPoint>[];

    for (final pair in basePairs) {
      final proj1 = _projectPoint(pair.point1, center);
      projectedPoints.add(ProjectedPoint(
        original: pair.point1,
        x: proj1.dx,
        y: proj1.dy,
        scale: proj1.scale,
      ));

      final proj2 = _projectPoint(pair.point2, center);
      projectedPoints.add(ProjectedPoint(
        original: pair.point2,
        x: proj2.dx,
        y: proj2.dy,
        scale: proj2.scale,
      ));
    }

    return projectedPoints;
  }

  _Projected2D _projectPoint(Point3D point, Offset center) {
    final rotatedX = point.x;
    final rotatedY = point.y * cos(tiltAngle) - point.z * sin(tiltAngle);
    final rotatedZ = point.y * sin(tiltAngle) + point.z * cos(tiltAngle);

    final distance = _cameraDistance + rotatedZ;
    final scale = _focalLength / distance;

    final x = center.dx + rotatedX * scale;
    final y = center.dy + rotatedY * scale;

    return _Projected2D(x, y, scale);
  }

  List<_SortedBasePair> _sortByDepth(
    List<BasePair> basePairs,
    List<ProjectedPoint> projectedPoints,
  ) {
    final sorted = <_SortedBasePair>[];

    for (int i = 0; i < basePairs.length; i++) {
      final proj1 = projectedPoints[i * 2];
      final proj2 = projectedPoints[i * 2 + 1];

      final avgDepth = (proj1.scale + proj2.scale) / 2;
      sorted.add(_SortedBasePair(
        pair: basePairs[i],
        idx1: i * 2,
        idx2: i * 2 + 1,
        avgDepth: avgDepth,
      ));
    }

    sorted.sort((a, b) => a.avgDepth.compareTo(b.avgDepth));

    return sorted;
  }

  void _drawHelix(
    Canvas canvas,
    List<_SortedBasePair> sortedBasePairs,
    List<ProjectedPoint> projectedPoints,
  ) {
    // 先绘制骨架
    _drawBackbone(canvas, projectedPoints);

    // 再绘制碱基对
    for (final entry in sortedBasePairs) {
      final idx1 = entry.idx1;
      final idx2 = entry.idx2;

      final proj1 = projectedPoints[idx1];
      final proj2 = projectedPoints[idx2];

      final avgScale = (proj1.scale + proj2.scale) / 2;
      final depth = (avgScale - 0.5).clamp(-0.5, 0.5);
      final alpha = (0.4 + depth).clamp(0.2, 1.0);
      final glowAlpha = (0.15 + depth * 0.4).clamp(0.05, 0.4);

      _drawHydrogenBonds(canvas, proj1, proj2, alpha);
      _drawBase(canvas, proj1, Colors.blue, alpha, glowAlpha, avgScale);
      _drawBase(canvas, proj2, Colors.pink, alpha, glowAlpha, avgScale);
    }
  }

  void _drawHydrogenBonds(
    Canvas canvas,
    ProjectedPoint proj1,
    ProjectedPoint proj2,
    double alpha,
  ) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.5 * ((proj1.scale + proj2.scale) / 2)
      ..color = Colors.cyan.withValues(alpha: alpha * 0.6);

    canvas.drawLine(
      Offset(proj1.x, proj1.y),
      Offset(proj2.x, proj2.y),
      paint,
    );
  }

  void _drawBase(
    Canvas canvas,
    ProjectedPoint proj,
    Color baseColor,
    double alpha,
    double glowAlpha,
    double scale,
  ) {
    final radius = 9 * scale;
    final glowRadius = 25 * scale;

    // 发光效果
    final glowPaint = Paint()
      ..color = baseColor.withValues(alpha: glowAlpha)
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
    canvas.drawCircle(
      Offset(proj.x, proj.y),
      glowRadius,
      glowPaint,
    );

    // 主体
    final paint = Paint()
      ..color = baseColor.withValues(alpha: alpha)
      ..style = PaintingStyle.fill;
    canvas.drawCircle(Offset(proj.x, proj.y), radius, paint);

    // 渐变高光
    final highlightPaint = Paint()
      ..shader = ui.Gradient.radial(
        Offset(proj.x - radius * 0.3, proj.y - radius * 0.3),
        radius * 0.5,
        [Colors.white.withValues(alpha: alpha * 0.6), Colors.transparent],
        [0.0, 1.0],
      )
      ..style = PaintingStyle.fill;
    canvas.drawCircle(
      Offset(proj.x - radius * 0.3, proj.y - radius * 0.3),
      radius * 0.5,
      highlightPaint,
    );

    // 外圈
    final ringPaint = Paint()
      ..color = Colors.white.withValues(alpha: alpha * 0.2)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;
    canvas.drawCircle(Offset(proj.x, proj.y), radius + 3, ringPaint);
  }

  void _drawBackbone(Canvas canvas, List<ProjectedPoint> projectedPoints) {
    // 绘制第一条链的渐变连线
    for (int i = 0; i < _basePairsCount - 1; i++) {
      final proj1 = projectedPoints[i * 2];
      final proj2 = projectedPoints[(i + 1) * 2];

      final paint = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 4 * ((proj1.scale + proj2.scale) / 2)
        ..color = Colors.blue.withValues(alpha: 0.4)
        ..strokeCap = StrokeCap.round;

      canvas.drawLine(
        Offset(proj1.x, proj1.y),
        Offset(proj2.x, proj2.y),
        paint,
      );
    }

    // 绘制第二条链的渐变连线
    for (int i = 0; i < _basePairsCount - 1; i++) {
      final proj1 = projectedPoints[i * 2 + 1];
      final proj2 = projectedPoints[(i + 1) * 2 + 1];

      final paint = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 4 * ((proj1.scale + proj2.scale) / 2)
        ..color = Colors.pink.withValues(alpha: 0.4)
        ..strokeCap = StrokeCap.round;

      canvas.drawLine(
        Offset(proj1.x, proj1.y),
        Offset(proj2.x, proj2.y),
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(covariant DNAHelixPainter oldDelegate) {
    return oldDelegate.rotation != rotation ||
        oldDelegate.tiltAngle != tiltAngle;
  }
}

class Point3D {
  final double x;
  final double y;
  final double z;

  Point3D(this.x, this.y, this.z);
}

class BasePair {
  final Point3D point1;
  final Point3D point2;
  final int pairIndex;

  BasePair({
    required this.point1,
    required this.point2,
    required this.pairIndex,
  });
}

class ProjectedPoint {
  final Point3D original;
  final double x;
  final double y;
  final double scale;

  ProjectedPoint({
    required this.original,
    required this.x,
    required this.y,
    required this.scale,
  });
}

class _Projected2D {
  final double dx;
  final double dy;
  final double scale;

  _Projected2D(this.dx, this.dy, this.scale);
}

class _SortedBasePair {
  final BasePair pair;
  final int idx1;
  final int idx2;
  final double avgDepth;

  _SortedBasePair({
    required this.pair,
    required this.idx1,
    required this.idx2,
    required this.avgDepth,
  });
}
相关推荐
马尔代夫哈哈哈9 小时前
Spring IoC&DI
数据库·sql
迅筑科技-RPT10 小时前
达索系统第7代解决方案3D UNIV+RSES——释放企业数据潜能,拥抱更值得信赖的工业AI
3d·ai·达索系统·迅筑科技·3d univ+rses
a11177610 小时前
医院挂号预约系统(开源 Fastapi+vue2)
前端·vue.js·python·html5·fastapi
中科米堆10 小时前
3D扫描仪如何悄悄改变我们的生活?
3d·自动化·3d全尺寸检测
液态不合群10 小时前
[特殊字符] MySQL 覆盖索引详解
数据库·mysql
0思必得010 小时前
[Web自动化] Selenium处理iframe和frame
前端·爬虫·python·selenium·自动化·web自动化
计算机毕设VX:Fegn089511 小时前
计算机毕业设计|基于springboot + vue蛋糕店管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
瀚高PG实验室11 小时前
PostgreSQL到HighgoDB数据迁移
数据库·postgresql·瀚高数据库
灰灰勇闯IT11 小时前
Flutter for OpenHarmony:图标与 Asset 资源管理 —— 构建高性能、可维护的视觉资源体系
flutter