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仅当rotation或tiltAngle变化时重绘; - 帧率稳定 :
AnimationControllervsync 同步屏幕刷新; - 对象复用:碱基对在每帧重新计算,但无内存分配高峰。
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,
});
}