分构App Canvas3D功能技术解析
如果你对3D分子可视化感兴趣,可以去鸿蒙应用市场搜索"分构"下载体验一下。今天咱们聊聊这个App的Canvas3D功能。
写在前面
大家好,今天聊的分构App,是一个3D分子结构可视化工具。这个App的技术含量比较高,涉及到3D图形渲染、数学计算这些Web前端很少深入的领域。
Web端做3D,有Three.js、Babylon.js这些成熟的库。鸿蒙端没有直接对应的3D库,但通过Canvas 2D的变换矩阵,我们也能实现简单的3D效果。当然,如果要做复杂的3D场景,可能需要考虑OpenGL或Vulkan,但那些就超出本文的范围了。
今天这篇,我会用Canvas 2D来实现一个简单的3D引擎,展示分子结构。虽然是简化版的,但核心的3D变换原理是一样的。
1. 3D基础:坐标系与投影
3D渲染的第一步是理解坐标系和投影。
3D坐标变换:
// 3D点定义
interface Point3D {
x: number;
y: number;
z: number;
}
interface Point2D {
x: number;
y: number;
}
class Transform3D {
// 透视投影
static perspective(point: Point3D, fov: number, distance: number): Point2D {
const factor = fov / (distance + point.z);
return {
x: point.x * factor,
y: point.y * factor
};
}
// 绕X轴旋转
static rotateX(point: Point3D, angle: number): Point3D {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return {
x: point.x,
y: point.y * cos - point.z * sin,
z: point.y * sin + point.z * cos
};
}
// 绕Y轴旋转
static rotateY(point: Point3D, angle: number): Point3D {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return {
x: point.x * cos + point.z * sin,
y: point.y,
z: -point.x * sin + point.z * cos
};
}
// 绕Z轴旋转
static rotateZ(point: Point3D, angle: number): Point3D {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return {
x: point.x * cos - point.y * sin,
y: point.x * sin + point.y * cos,
z: point.z
};
}
// 平移
static translate(point: Point3D, dx: number, dy: number, dz: number): Point3D {
return {
x: point.x + dx,
y: point.y + dy,
z: point.z + dz
};
}
// 缩放
static scale(point: Point3D, sx: number, sy: number, sz: number): Point3D {
return {
x: point.x * sx,
y: point.y * sy,
z: point.z * sz
};
}
}
2. 分子结构:数据模型
分子由原子和化学键组成。
分子数据结构:
interface Atom {
id: string;
element: string;
position: Point3D;
color: string;
radius: number;
}
interface Bond {
id: string;
atom1Id: string;
atom2Id: string;
type: 'single' | 'double' | 'triple';
}
interface Molecule {
id: string;
name: string;
atoms: Atom[];
bonds: Bond[];
}
// 元素属性
const elementProperties: Record<string, { color: string; radius: number }> = {
'H': { color: '#FFFFFF', radius: 0.31 },
'C': { color: '#333333', radius: 0.77 },
'N': { color: '#3050F8', radius: 0.75 },
'O': { color: '#FF0D0D', radius: 0.73 },
'S': { color: '#FFFF30', radius: 1.02 },
'P': { color: '#FF8000', radius: 1.05 },
'Cl': { color: '#1FF01F', radius: 0.99 },
'Br': { color: '#A62929', radius: 1.14 }
};
// 示例分子:水
const waterMolecule: Molecule = {
id: 'water',
name: '水分子 (H₂O)',
atoms: [
{ id: 'O1', element: 'O', position: { x: 0, y: 0, z: 0 }, ...elementProperties['O'] },
{ id: 'H1', element: 'H', position: { x: -0.96, y: 0, z: 0 }, ...elementProperties['H'] },
{ id: 'H2', element: 'H', position: { x: 0.24, y: 0.93, z: 0 }, ...elementProperties['H'] }
],
bonds: [
{ id: 'b1', atom1Id: 'O1', atom2Id: 'H1', type: 'single' },
{ id: 'b2', atom1Id: 'O1', atom2Id: 'H2', type: 'single' }
]
};
3. 3D渲染:Canvas绘制分子
用Canvas 2D绘制3D分子结构。
ArkTS分子渲染器:
@Component
struct MoleculeRenderer {
@State molecule: Molecule = waterMolecule;
@State rotationX: number = 0;
@State rotationY: number = 0;
@State rotationZ: number = 0;
@State scale: number = 100;
@State isAutoRotating: boolean = false;
private centerX: number = 150;
private centerY: number = 200;
private fov: number = 500;
private distance: number = 5;
build() {
Column() {
Canvas(this.drawMolecule)
.width('90%')
.height(400)
.backgroundColor('#1a1a2e')
.borderRadius(12)
.margin({ top: 20 })
// 控制面板
Column() {
Row() {
Text('X轴旋转')
.fontSize(14)
.width(80)
Slider({
value: this.rotationX,
min: -Math.PI,
max: Math.PI,
step: 0.01
})
.onChange((value: number) => {
this.rotationX = value;
})
}
Row() {
Text('Y轴旋转')
.fontSize(14)
.width(80)
Slider({
value: this.rotationY,
min: -Math.PI,
max: Math.PI,
step: 0.01
})
.onChange((value: number) => {
this.rotationY = value;
})
}
Row() {
Text('缩放')
.fontSize(14)
.width(80)
Slider({
value: this.scale,
min: 50,
max: 200,
step: 1
})
.onChange((value: number) => {
this.scale = value;
})
}
}
.width('90%')
.margin({ top: 16 })
// 操作按钮
Row() {
Button(this.isAutoRotating ? '停止旋转' : '自动旋转')
.onClick(() => this.toggleAutoRotation())
Button('重置视角')
.margin({ left: 12 })
.onClick(() => this.resetView())
}
.margin({ top: 16 })
}
}
drawMolecule = (ctx: CanvasRenderingContext2D) => {
const width = 300;
const height = 400;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 变换原子位置
const transformedAtoms = this.molecule.atoms.map(atom => {
let pos = { ...atom.position };
// 应用旋转变换
pos = Transform3D.rotateX(pos, this.rotationX);
pos = Transform3D.rotateY(pos, this.rotationY);
pos = Transform3D.rotateZ(pos, this.rotationZ);
// 应用缩放
pos = Transform3D.scale(pos, this.scale, this.scale, this.scale);
// 透视投影
const projected = Transform3D.perspective(pos, this.fov, this.distance);
return {
...atom,
screenX: projected.x + this.centerX,
screenY: projected.y + this.centerY,
depth: pos.z
};
});
// 按深度排序(远的先画)
const sortedAtoms = [...transformedAtoms].sort((a, b) => a.depth - b.depth);
// 绘制化学键
this.molecule.bonds.forEach(bond => {
const atom1 = transformedAtoms.find(a => a.id === bond.atom1Id);
const atom2 = transformedAtoms.find(a => a.id === bond.atom2Id);
if (atom1 && atom2) {
this.drawBond(ctx, atom1, atom2, bond.type);
}
});
// 绘制原子
sortedAtoms.forEach(atom => {
this.drawAtom(ctx, atom);
});
}
drawAtom(ctx: CanvasRenderingContext2D, atom: any) {
// 绘制阴影
ctx.beginPath();
ctx.arc(atom.screenX + 2, atom.screenY + 2, atom.radius * this.scale * 0.3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fill();
// 绘制原子
ctx.beginPath();
ctx.arc(atom.screenX, atom.screenY, atom.radius * this.scale * 0.3, 0, Math.PI * 2);
// 创建渐变效果
const gradient = ctx.createRadialGradient(
atom.screenX - 5, atom.screenY - 5, 0,
atom.screenX, atom.screenY, atom.radius * this.scale * 0.3
);
gradient.addColorStop(0, this.lightenColor(atom.color, 50));
gradient.addColorStop(1, atom.color);
ctx.fillStyle = gradient;
ctx.fill();
// 绘制边框
ctx.strokeStyle = this.darkenColor(atom.color, 30);
ctx.lineWidth = 1;
ctx.stroke();
// 绘制元素符号
ctx.fillStyle = '#fff';
ctx.font = `${Math.max(10, atom.radius * this.scale * 0.2)}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(atom.element, atom.screenX, atom.screenY);
}
drawBond(ctx: CanvasRenderingContext2D, atom1: any, atom2: any, type: string) {
const dx = atom2.screenX - atom1.screenX;
const dy = atom2.screenY - atom1.screenY;
ctx.strokeStyle = '#666';
ctx.lineWidth = 2;
switch (type) {
case 'single':
ctx.beginPath();
ctx.moveTo(atom1.screenX, atom1.screenY);
ctx.lineTo(atom2.screenX, atom2.screenY);
ctx.stroke();
break;
case 'double':
const offset = 3;
const nx = -dy / Math.sqrt(dx * dx + dy * dy) * offset;
const ny = dx / Math.sqrt(dx * dx + dy * dy) * offset;
ctx.beginPath();
ctx.moveTo(atom1.screenX + nx, atom1.screenY + ny);
ctx.lineTo(atom2.screenX + nx, atom2.screenY + ny);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(atom1.screenX - nx, atom1.screenY - ny);
ctx.lineTo(atom2.screenX - nx, atom2.screenY - ny);
ctx.stroke();
break;
case 'triple':
// 类似double,但多一条线
break;
}
}
lightenColor(color: string, amount: number): string {
// 简化实现
return color;
}
darkenColor(color: string, amount: number): string {
// 简化实现
return color;
}
toggleAutoRotation() {
this.isAutoRotating = !this.isAutoRotating;
if (this.isAutoRotating) {
this.autoRotate();
}
}
autoRotate() {
if (!this.isAutoRotating) return;
this.rotationY += 0.02;
setTimeout(() => this.autoRotate(), 16);
}
resetView() {
this.rotationX = 0;
this.rotationY = 0;
this.rotationZ = 0;
this.scale = 100;
}
}
4. 触摸交互:手势控制
用户可以通过手势来旋转和缩放分子。
ArkTS手势处理:
@Component
struct InteractiveMolecule {
@State rotationX: number = 0;
@State rotationY: number = 0;
@State scale: number = 1;
@State lastTouchX: number = 0;
@State lastTouchY: number = 0;
@State lastPinchDistance: number = 0;
build() {
Column() {
MoleculeRenderer({
rotationX: this.rotationX,
rotationY: this.rotationY,
scale: this.scale * 100
})
}
.onTouch((event: TouchEvent) => {
this.handleTouch(event);
})
}
handleTouch(event: TouchEvent) {
switch (event.type) {
case TouchType.Down:
if (event.touches.length === 1) {
this.lastTouchX = event.touches[0].x;
this.lastTouchY = event.touches[0].y;
} else if (event.touches.length === 2) {
this.lastPinchDistance = this.getPinchDistance(event.touches);
}
break;
case TouchType.Move:
if (event.touches.length === 1) {
// 单指旋转
const dx = event.touches[0].x - this.lastTouchX;
const dy = event.touches[0].y - this.lastTouchY;
this.rotationY += dx * 0.01;
this.rotationX += dy * 0.01;
this.lastTouchX = event.touches[0].x;
this.lastTouchY = event.touches[0].y;
} else if (event.touches.length === 2) {
// 双指缩放
const currentDistance = this.getPinchDistance(event.touches);
const scaleChange = currentDistance / this.lastPinchDistance;
this.scale *= scaleChange;
this.scale = Math.max(0.5, Math.min(3, this.scale));
this.lastPinchDistance = currentDistance;
}
break;
}
}
getPinchDistance(touches: TouchObject[]): number {
const dx = touches[1].x - touches[0].x;
const dy = touches[1].y - touches[0].y;
return Math.sqrt(dx * dx + dy * dy);
}
}
5. 分子库:预设分子
App内置了一些常见分子供用户学习。
分子数据库:
const moleculeDatabase: Molecule[] = [
// 水 H₂O
{
id: 'water',
name: '水 (H₂O)',
atoms: [
{ id: 'O', element: 'O', position: { x: 0, y: 0, z: 0 }, color: '#FF0D0D', radius: 0.73 },
{ id: 'H1', element: 'H', position: { x: -0.96, y: 0, z: 0 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H2', element: 'H', position: { x: 0.24, y: 0.93, z: 0 }, color: '#FFFFFF', radius: 0.31 }
],
bonds: [
{ id: 'b1', atom1Id: 'O', atom2Id: 'H1', type: 'single' },
{ id: 'b2', atom1Id: 'O', atom2Id: 'H2', type: 'single' }
]
},
// 二氧化碳 CO₂
{
id: 'co2',
name: '二氧化碳 (CO₂)',
atoms: [
{ id: 'C', element: 'C', position: { x: 0, y: 0, z: 0 }, color: '#333333', radius: 0.77 },
{ id: 'O1', element: 'O', position: { x: -1.16, y: 0, z: 0 }, color: '#FF0D0D', radius: 0.73 },
{ id: 'O2', element: 'O', position: { x: 1.16, y: 0, z: 0 }, color: '#FF0D0D', radius: 0.73 }
],
bonds: [
{ id: 'b1', atom1Id: 'C', atom2Id: 'O1', type: 'double' },
{ id: 'b2', atom1Id: 'C', atom2Id: 'O2', type: 'double' }
]
},
// 甲烷 CH₄
{
id: 'methane',
name: '甲烷 (CH₄)',
atoms: [
{ id: 'C', element: 'C', position: { x: 0, y: 0, z: 0 }, color: '#333333', radius: 0.77 },
{ id: 'H1', element: 'H', position: { x: 0.63, y: 0.63, z: 0.63 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H2', element: 'H', position: { x: -0.63, y: -0.63, z: 0.63 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H3', element: 'H', position: { x: -0.63, y: 0.63, z: -0.63 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H4', element: 'H', position: { x: 0.63, y: -0.63, z: -0.63 }, color: '#FFFFFF', radius: 0.31 }
],
bonds: [
{ id: 'b1', atom1Id: 'C', atom2Id: 'H1', type: 'single' },
{ id: 'b2', atom1Id: 'C', atom2Id: 'H2', type: 'single' },
{ id: 'b3', atom1Id: 'C', atom2Id: 'H3', type: 'single' },
{ id: 'b4', atom1Id: 'C', atom2Id: 'H4', type: 'single' }
]
},
// 乙醇 C₂H₅OH
{
id: 'ethanol',
name: '乙醇 (C₂H₅OH)',
atoms: [
{ id: 'C1', element: 'C', position: { x: -0.75, y: 0, z: 0 }, color: '#333333', radius: 0.77 },
{ id: 'C2', element: 'C', position: { x: 0.75, y: 0, z: 0 }, color: '#333333', radius: 0.77 },
{ id: 'O', element: 'O', position: { x: 1.5, y: 1.0, z: 0 }, color: '#FF0D0D', radius: 0.73 },
{ id: 'H1', element: 'H', position: { x: -1.2, y: 0.9, z: 0 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H2', element: 'H', position: { x: -1.2, y: -0.9, z: 0 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H3', element: 'H', position: { x: -1.2, y: 0, z: 0.9 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H4', element: 'H', position: { x: 1.0, y: -0.9, z: 0 }, color: '#FFFFFF', radius: 0.31 },
{ id: 'H5', element: 'H', position: { x: 2.3, y: 1.0, z: 0 }, color: '#FFFFFF', radius: 0.31 }
],
bonds: [
{ id: 'b1', atom1Id: 'C1', atom2Id: 'C2', type: 'single' },
{ id: 'b2', atom1Id: 'C2', atom2Id: 'O', type: 'single' },
{ id: 'b3', atom1Id: 'C1', atom2Id: 'H1', type: 'single' },
{ id: 'b4', atom1Id: 'C1', atom2Id: 'H2', type: 'single' },
{ id: 'b5', atom1Id: 'C1', atom2Id: 'H3', type: 'single' },
{ id: 'b6', atom1Id: 'C2', atom2Id: 'H4', type: 'single' },
{ id: 'b7', atom1Id: 'O', atom2Id: 'H5', type: 'single' }
]
}
];
总结
分构App的Canvas3D功能,通过Canvas 2D实现了简单的3D分子可视化。虽然没有专业的3D引擎,但核心的坐标变换、透视投影、深度排序这些概念都涵盖了。
如果你对3D编程感兴趣,建议从2D Canvas开始,理解了基本的数学原理后,再转向专业的3D库会更容易上手。
分构App就聊到这里。下一篇文章,我会聊聊分构App的分子数据管理功能。