功能预览
深度与密度测试
对于测试部分,分别对深度和密度进行了测试:
注意!!!!!!!只实现了识别Markdown中的#代表的层级,所以不能使用其余标识符!!!
注意!!!!!!!只实现了识别Markdown中的#代表的层级,所以不能使用其余标识符!!!
注意!!!!!!!只实现了识别Markdown中的#代表的层级,所以不能使用其余标识符!!!
注意!!!!!!!只实现了识别Markdown中的#代表的层级,所以不能使用其余标识符!!!
注意!!!!!!!只实现了识别Markdown中的#代表的层级,所以不能使用其余标识符!!!
测试数据:
# 量子力学基础
## 1. 基础概念
### 1.1 波粒二象性
#### 1.1.1 光的波粒二象性
##### 1.1.1.1 光电效应
###### 1.1.1.1.1 光电效应的实验现象
###### 1.1.1.1.2 爱因斯坦的光电效应理论
##### 1.1.1.2 康普顿效应
#### 1.1.2 微观粒子的波粒二象性
##### 1.1.2.1 德布罗意假设
##### 1.1.2.2 电子衍射实验
### 1.2 不确定性原理
#### 1.2.1 海森堡不确定性原理
##### 1.2.1.1 位置与动量的不确定性关系
##### 1.2.1.2 能量与时间的不确定性关系
#### 1.2.2 理解与应用
##### 1.2.2.1 对微观世界的解释
##### 1.2.2.2 在量子计算中的意义
## 2. 量子态与量子叠加
### 2.1 量子态的描述
#### 2.1.1 状态向量与希尔伯特空间
##### 2.1.1.1 状态向量的概念
##### 2.1.1.2 希尔伯特空间的基本性质
#### 2.1.2 波函数与薛定谔方程
##### 2.1.2.1 波函数的物理意义
##### 2.1.2.2 薛定谔方程的形式与解法
### 2.2 量子叠加原理
#### 2.2.1 叠加态的概念
##### 2.2.1.1 叠加态的数学表示
##### 2.2.1.2 叠加态的实验验证(双缝实验)
#### 2.2.2 叠加态的应用
##### 2.2.2.1 在量子通信中的应用
##### 2.2.2.2 在量子计算中的应用
## 3. 量子纠缠与非局域性
### 3.1 量子纠缠的概念
#### 3.1.1 纠缠态的定义与分类
##### 3.1.1.1 Bell态
##### 3.1.1.2 GHZ态
#### 3.1.2 纠缠态的实验验证
##### 3.1.2.1 EPR悖论
##### 3.1.2.2 Bell不等式与实验结果
### 3.2 非局域性与量子通信
#### 3.2.1 非局域性的物理意义
##### 3.2.1.1 非局域性的实验验证
##### 3.2.1.2 非局域性在量子隐形传态中的应用
#### 3.2.2 量子通信的基本原理
##### 3.2.2.1 量子密钥分发(QKD)
##### 3.2.2.2 量子隐形传态(Quantum Teleportation)
测试结果:
一、工具概述
功能就是上传Markdown格式文件,然后转换为脑图,然后下载,没有添加其余功能了。
我觉得还可以添加:
1、为脑图添加标题。
2、现在的脑图颜色、连接方式单一,可以增加更多的样式。
3。。。。。。。。。再想想。
算了,本来也就是做一个样例,再想下去就快想出来一个成品了。。。。
半残不残的挺好的。。。。
另外,现在没做优化,所以,如果你直接copy代码的话,可能会出现一些内存占用的情况。
二、代码结构划分
1. HTML 结构
<div class="container">
<div class="upload-area">
<!-- 文件上传区域 -->
<label for="markdownFile" class="upload-label">上传 Markdown 文件</label>
<input type="file" id="markdownFile" accept=".md,.markdown" hidden>
</div>
<div class="mindmap-container">
<canvas id="canvas"></canvas>
</div>
</div>
- 定义了页面的基本布局,包括文件上传区域和画布容器。
- 使用
canvas
元素作为图形渲染的主要载体。
2. CSS 样式
:root {
--primary-color: #2196F3;
--secondary-color: #4CAF50;
--background: #f8f9fa;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
margin: 0;
background: var(--background);
}
- 定义了主题颜色、字体和背景样式。
- 实现了响应式布局和交互效果(如悬停动画)。
3. JavaScript 核心逻辑
class MindmapRenderer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.scale = 1;
this.offsetX = 0;
this.offsetY = 0;
this.nodes = [];
this.initEvents();
}
MindmapRenderer
类负责整个渲染流程,包括节点绘制、布局计算、交互操作(缩放和平移)。parseMarkdown
函数将 Markdown 文件解析为树状节点结构。exportHighResImage
函数实现高分辨率图片导出功能。
三、功能实现
1. 核心功能
(1)Markdown 转思维导图
- 支持将 Markdown 文件中的标题(
#
符号)层级结构转换为树状结构。 - 示例:
转换后生成如下结构:# 根节点 ## 子节点1 ### 孙节点1 ## 子节点2
根节点 ├── 子节点1 │ └── 孙节点1 └── 子节点2
(2)节点渲染
- 每个节点根据层级(1-10)使用不同的样式(颜色、字体大小、圆角等)。
- 示例:
const NODE_STYLES = { 1: { bg: "#2962FF", text: "#fff", fontSize: 20 }, // 根节点样式 2: { bg: "#00C853", text: "#fff", fontSize: 19 }, // 子节点样式 // ... };
(3)交互操作
- 缩放和平移:用户可以通过鼠标滚轮缩放画布,拖拽画布进行平移。
- 导出图片:支持将当前视图导出为高分辨率的 JPEG 图片。
(4)自动布局
- 使用递归算法计算节点位置和大小。
- 动态调整子树宽度和高度,避免节点重叠。
2. 功能亮点
(1)动态布局算法
- 子树测量:递归测量每个节点及其子树的宽度和高度。
- 碰撞检测:当节点过多时,自动调整位置避免重叠。
- 压缩因子:优化节点布局,减少垂直方向的空间占用。
(2)交互体验
- 平滑缩放和平移:使用 CSS 3D 变换来实现流畅的操作。
- 高分辨率导出:导出的图片保留所有细节,适合打印或分享。
(3)自适应设计
- 画布自适应:根据内容自动调整画布大小。
- 响应式布局:使用
ResizeObserver
监听容器大小变化并自动调整。
(4)性能优化
- 渲染性能:通过合理布局减少重绘次数。
- 事件处理:使用事件委托优化交互操作。
四、技术栈
1. 前端技术
- HTML5 Canvas: 用于绘制复杂的图形和节点。
- CSS3: 实现响应式布局和交互效果。
- JavaScript ES6+: 使用现代 JavaScript 特性(如类、箭头函数等)。
2. 数据处理
- Markdown 解析: 自行实现的解析器,支持多级标题嵌套。
- 树形数据结构: 将 Markdown 文件转换为树形节点结构。
3. 交互技术
- 事件监听: 处理鼠标拖拽、滚轮缩放等操作。
- Canvas 缩放和平移: 使用
setTransform
方法实现复杂变换。
4. 图形绘制
- Bezier 曲线: 绘制节点之间的连接线。
- 文字换行: 在节点内实现文字自动换行。
五、当前缺点
1. 性能问题
- 处理大量节点时,渲染性能可能下降。
- 缩放和平移操作在复杂场景下可能出现延迟。
2. 功能限制
- Markdown 支持有限: 仅支持标题(
#
)语法,不支持其他 Markdown 元素(如列表、图片等)。 - 缺乏编辑功能: 无法直接在画布上编辑节点内容。
- 导出格式单一: 仅支持 JPEG 格式导出。
3. 用户体验
- 缺少加载进度提示,大文件上传时可能会出现卡顿。
- 缩放和平移操作的手感有待优化(如增加惯性滚动)。
4. 代码结构
- 部分逻辑耦合度较高,维护成本较高。
- 缺少单元测试和文档注释,代码可读性有待提升。
六、未来改进方向
1. 功能扩展
- 支持更多 Markdown 语法(如列表、图片、链接等)。
- 增加节点编辑功能(如拖拽调整大小、修改文字内容)。
- 支持更多导出格式(如 PNG、SVG)。
2. 性能优化
- 使用 Web Workers 分担后台计算任务。
- 优化碰撞检测算法,减少计算量。
3. 用户体验提升
- 增加加载进度提示。
- 优化缩放和平移操作的手感(如增加惯性滚动)。
4. 代码重构
- 提取公共逻辑,降低代码耦合度。
- 增加单元测试和文档注释。
七、完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown转脑图</title>
<style>
:root {
--primary-color: #2196F3;
--secondary-color: #4CAF50;
--background: #f8f9fa;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
margin: 0;
background: var(--background);
}
.container {
max-width: 1800px;
margin: 20px auto;
padding: 20px;
}
.upload-area {
text-align: center;
margin-bottom: 30px;
position: relative;
}
.mindmap-container {
background: white;
border-radius: 16px;
box-shadow: 0 12px 32px rgba(0,0,0,0.1);
overflow: hidden;
height: 85vh;
position: relative;
}
#canvas {
cursor: grab;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.upload-label {
display: inline-flex;
align-items: center;
padding: 12px 28px;
background: var(--primary-color);
color: white;
border-radius: 10px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
font-weight: 500;
}
.upload-label:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(33,150,243,0.25);
}
.export-btn {
background: var(--secondary-color);
margin-left: 15px;
}
.export-btn:hover {
box-shadow: 0 6px 16px rgba(76,175,80,0.25);
}
</style>
</head>
<body>
<div class="container">
<div class="upload-area">
<label for="markdownFile" class="upload-label">
📁 上传 Markdown 文件
</label>
<button class="upload-label export-btn" id="exportBtn">📷 导出图片</button>
<input type="file" id="markdownFile" accept=".md,.markdown" hidden>
</div>
<div class="mindmap-container">
<canvas id="canvas"></canvas>
</div>
</div>
<script>
const NODE_STYLES = {
1: {
bg: "#2962FF",
text: "#fff",
minWidth: 160,
paddingX: 24,
paddingY: 16,
fontSize: 20,
rectRadius: 12
},
2: {
bg: "#00C853",
text: "#fff",
minWidth: 148,
paddingX: 22,
paddingY: 14,
fontSize: 19,
rectRadius: 10
},
3: {
bg: "#AA00FF",
text: "#fff",
minWidth: 136,
paddingX: 20,
paddingY: 12,
fontSize: 18,
rectRadius: 9
},
4: {
bg: "#FF6D00",
text: "#fff",
minWidth: 124,
paddingX: 18,
paddingY: 10,
fontSize: 17,
rectRadius: 8
},
5: {
bg: "#6A1B9A",
text: "#fff",
minWidth: 112,
paddingX: 16,
paddingY: 9,
fontSize: 16,
rectRadius: 7
},
6: {
bg: "#D50000",
text: "#fff",
minWidth: 100,
paddingX: 14,
paddingY: 8,
fontSize: 15,
rectRadius: 6
},
7: {
bg: "#00897B",
text: "#fff",
minWidth: 88,
paddingX: 12,
paddingY: 7,
fontSize: 14,
rectRadius: 5
},
8: {
bg: "#546E7A",
text: "#fff",
minWidth: 76,
paddingX: 10,
paddingY: 6,
fontSize: 13,
rectRadius: 4
},
9: {
bg: "#757575",
text: "#fff",
minWidth: 64,
paddingX: 8,
paddingY: 5,
fontSize: 12,
rectRadius: 3
},
10: {
bg: "#BDBDBD",
text: "#212121",
minWidth: 52,
paddingX: 6,
paddingY: 4,
fontSize: 11,
rectRadius: 2
},
default: {
bg: "#607D8B",
text: "#fff",
minWidth: 60,
paddingX: 4,
paddingY: 4,
fontSize: 10,
rectRadius: 2
}
};
class MindmapRenderer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.scale = 1;
this.offsetX = 0;
this.offsetY = 0;
this.nodes = [];
this.LAYOUT_CONFIG = {
BASE_GAP_X: 60,
BASE_GAP_Y: 25,
DEPTH_REDUCTION: 1.4,
MIN_SIBLING_GAP: 15,
LINE_HEIGHT_RATIO: 1.2,
DEPTH_WIDTH: 80
};
this.initEvents();
}
initEvents() {
let isDragging = false;
let lastX = 0, lastY = 0;
const handleStart = e => {
isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
this.canvas.style.cursor = 'grabbing';
};
const handleMove = e => {
if (isDragging) {
const dx = (e.clientX - lastX) / this.scale;
const dy = (e.clientY - lastY) / this.scale;
this.offsetX += dx;
this.offsetY += dy;
lastX = e.clientX;
lastY = e.clientY;
this.render();
}
};
const handleEnd = () => {
isDragging = false;
this.canvas.style.cursor = 'grab';
};
this.canvas.addEventListener('mousedown', handleStart);
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', handleEnd);
this.canvas.addEventListener('wheel', e => {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const mouseX = (e.clientX - rect.left - this.offsetX) / this.scale;
const mouseY = (e.clientY - rect.top - this.offsetY) / this.scale;
const zoom = e.deltaY < 0 ? 1.1 : 0.9;
this.scale = Math.min(Math.max(this.scale * zoom, 0.3), 5);
this.offsetX = (e.clientX - rect.left - mouseX * this.scale);
this.offsetY = (e.clientY - rect.top - mouseY * this.scale);
this.render();
});
}
getNodeDepth(node) {
let depth = 0;
let current = node;
while (current.parent) {
depth++;
current = current.parent;
}
return depth;
}
measureSubtree(node) {
const ctx = this.ctx;
node.size = this.calculateNodeSize(node, ctx);
if (node.children.length === 0) {
node.subtreeWidth = node.size.width;
node.subtreeHeight = node.size.height;
return;
}
let totalHeight = 0;
let maxChildRight = 0; // 最大右侧位置
const depth = this.getNodeDepth(node);
const dynamicGap = this.LAYOUT_CONFIG.BASE_GAP_Y
* Math.pow(this.LAYOUT_CONFIG.DEPTH_REDUCTION, depth);
node.children.forEach((child, index) => {
this.measureSubtree(child);
totalHeight += child.subtreeHeight;
// 父右侧 + 间距 + 子节点宽度
const childRight = this.LAYOUT_CONFIG.DEPTH_WIDTH + child.size.width;
maxChildRight = Math.max(maxChildRight, childRight);
if (index !== node.children.length - 1) {
totalHeight += dynamicGap;
}
});
node.subtreeHeight = Math.max(node.size.height, totalHeight);
// 子树宽度 = 父节点半宽 + 最大子节点右侧
node.subtreeWidth = node.size.width / 2 + maxChildRight;
}
calculateNodeSize(node, ctx) {
const style = NODE_STYLES[node.level] || NODE_STYLES.default;
ctx.font = `${style.fontSize}px 'Segoe UI'`;
const textMetrics = ctx.measureText(node.text);
const contentWidth = textMetrics.width + style.paddingX * 2;
const width = Math.max(style.minWidth, contentWidth);
const lineHeight = style.fontSize * this.LAYOUT_CONFIG.LINE_HEIGHT_RATIO;
const lines = Math.ceil(textMetrics.width / (width - style.paddingX * 2));
const height = Math.max(style.minWidth * 0.6, lines * lineHeight + style.paddingY * 2);
return { width, height };
}
calculateLayout(nodes) {
const layoutNode = (node, startX, startY) => {
node.x = startX;
node.y = startY;
if (node.children.length === 0) return;
let currentY = startY - node.subtreeHeight / 2;
const depth = this.getNodeDepth(node);
const compressFactor = node.children.length > 3 ? 0.9 : 1;
node.children.forEach(child => {
// 修正子节点定位
const parentRightEdge = node.x + node.size.width / 2;
const childX = parentRightEdge + this.LAYOUT_CONFIG.DEPTH_WIDTH
+ child.size.width / 2;
const childY = currentY + child.subtreeHeight / 2 * compressFactor;
layoutNode(child, childX, childY);
currentY += child.subtreeHeight * compressFactor
+ this.LAYOUT_CONFIG.MIN_SIBLING_GAP;
});
};
nodes.forEach(root => {
this.measureSubtree(root);
layoutNode(root, 100, this.canvas.height / 2 / this.scale);
});
this.resolveCollisions(nodes);
}
resolveCollisions(nodes) {
const findConflicts = (nodeList) => {
nodeList.forEach((node, i) => {
for (let j = i + 1; j < nodeList.length; j++) {
const other = nodeList[j];
if (this.checkCollision(node, other)) {
const offset = node.size.height + this.LAYOUT_CONFIG.MIN_SIBLING_GAP;
other.y += offset;
this.updateAncestorsPosition(other);
}
}
if (node.children.length > 0) {
findConflicts(node.children);
}
});
};
findConflicts(nodes);
}
checkCollision(a, b) {
return Math.abs(a.x - b.x) < (a.size.width + b.size.width)/2 &&
Math.abs(a.y - b.y) < (a.size.height + b.size.height)/2;
}
updateAncestorsPosition(node) {
let current = node.parent;
while (current) {
current.y = node.y;
current = current.parent;
}
}
drawNode(node) {
const style = NODE_STYLES[node.level] || NODE_STYLES.default;
const ctx = this.ctx;
ctx.shadowColor = 'rgba(0,0,0,0.1)';
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 3;
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.roundRect(
node.x - node.size.width / 2,
node.y - node.size.height / 2,
node.size.width,
node.size.height,
style.rectRadius
);
ctx.fillStyle = style.bg;
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.fillStyle = style.text;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `${style.fontSize}px 'Segoe UI'`;
this.wrapText(node.text,
node.x, node.y,
node.size.width - style.paddingX * 2,
style.fontSize * 1.4
);
}
wrapText(text, x, y, maxWidth, lineHeight) {
const words = text.split(' ');
let currentLine = '';
let currentY = y - (words.length > 1 ? lineHeight / 2 : 0);
for (const word of words) {
const testLine = currentLine ? `${currentLine} ${word}` : word;
const metrics = this.ctx.measureText(testLine);
if (metrics.width > maxWidth && currentLine) {
this.ctx.fillText(currentLine, x, currentY);
currentLine = word;
currentY += lineHeight;
} else {
currentLine = testLine;
}
}
this.ctx.fillText(currentLine, x, currentY);
}
drawConnection(parent, child) {
const ctx = this.ctx;
const parentRight = parent.x + parent.size.width / 2;
const childLeft = child.x - child.size.width / 2;
const controlX = (parentRight + childLeft) / 2;
ctx.beginPath();
ctx.moveTo(parentRight, parent.y);
ctx.bezierCurveTo(
controlX, parent.y,
controlX, child.y,
childLeft, child.y
);
ctx.strokeStyle = parent.level === 1 ? '#78909C' : '#B0BEC5';
ctx.lineWidth = 2;
ctx.stroke();
}
render() {
this.ctx.save();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.scale(this.scale, this.scale);
this.ctx.translate(this.offsetX, this.offsetY);
this.traverseNodes(node => this.drawNode(node));
this.traverseNodes(node => {
node.children.forEach(child => this.drawConnection(node, child));
});
this.ctx.restore();
}
traverseNodes(callback) {
const traverse = node => {
callback(node);
node.children.forEach(traverse);
};
this.nodes.forEach(traverse);
}
}
// 页面集成
const canvas = document.getElementById('canvas');
const renderer = new MindmapRenderer(canvas);
const container = document.querySelector('.mindmap-container');
function adaptiveResize() {
const computeCanvasSize = () => {
if (!renderer.nodes.length) {
return [container.clientWidth, container.clientHeight];
}
let minX = Infinity, maxX = -Infinity;
let minY = Infinity, maxY = -Infinity;
renderer.traverseNodes(node => {
minX = Math.min(minX, node.x - node.size.width/2);
maxX = Math.max(maxX, node.x + node.size.width/2);
minY = Math.min(minY, node.y - node.size.height/2);
maxY = Math.max(maxY, node.y + node.size.height/2);
});
return [
Math.max(container.clientWidth, (maxX - minX) * 1.2 * renderer.scale),
Math.max(container.clientHeight, (maxY - minY) * 1.2 * renderer.scale)
];
};
const [newWidth, newHeight] = computeCanvasSize();
canvas.width = newWidth;
canvas.height = newHeight;
renderer.render();
}
function resizeCanvas() {
if (!renderer.nodes.length) {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
return;
}
let minX = Infinity, maxX = -Infinity;
let minY = Infinity, maxY = -Infinity;
renderer.traverseNodes(node => {
minX = Math.min(minX, node.x - node.size.width/2);
maxX = Math.max(maxX, node.x + node.size.width/2);
minY = Math.min(minY, node.y - node.size.height/2);
maxY = Math.max(maxY, node.y + node.size.height/2);
});
canvas.width = Math.max(container.clientWidth, (maxX - minX) * 1.2 * renderer.scale);
canvas.height = Math.max(container.clientHeight, (maxY - minY) * 1.2 * renderer.scale);
renderer.render();
}
// 响应式处理
const resizeObserver = new ResizeObserver(() => adaptiveResize());
resizeObserver.observe(container);
// 文件处理
document.getElementById('markdownFile').addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
const text = await file.text();
renderer.nodes = parseMarkdown(text);
renderer.calculateLayout(renderer.nodes);
adaptiveResize();
});
// Markdown解析
function parseMarkdown(content) {
const lines = content.split('\n').filter(l => l.trim());
const rootNodes = [];
const stack = [];
let lastLevel = 0;
lines.forEach(line => {
const match = line.match(/^(#+)\s*(.*)/);
if (!match) return;
const level = match[1].length;
const node = {
text: match[2].trim(),
level: level,
children: [],
parent: null
};
// 层级关系处理
if (level > lastLevel) {
if (stack.length > 0) {
node.parent = stack[stack.length - 1];
node.parent.children.push(node);
}
} else {
while (stack.length && stack[stack.length - 1].level >= level) {
stack.pop();
}
if (stack.length) {
node.parent = stack[stack.length - 1];
node.parent.children.push(node);
}
}
if (!node.parent) rootNodes.push(node);
stack.push(node);
lastLevel = level;
});
return rootNodes;
}
function exportHighResImage() {
const exportCanvas = document.createElement("canvas");
const exportCtx = exportCanvas.getContext("2d");
// 计算全图边界
let minX = Infinity, maxX = -Infinity;
let minY = Infinity, maxY = -Infinity;
renderer.traverseNodes(node => {
const halfWidth = node.size.width / 2;
const halfHeight = node.size.height / 2;
minX = Math.min(minX, node.x - halfWidth);
maxX = Math.max(maxX, node.x + halfWidth);
minY = Math.min(minY, node.y - halfHeight);
maxY = Math.max(maxY, node.y + halfHeight);
});
// 设置画布尺寸
const padding_left = 100;
const padding_top = 80;
exportCanvas.width = (maxX - minX) + padding_left * 2;
exportCanvas.height = (maxY - minY) + padding_top * 2;
// 填充白色背景
exportCtx.fillStyle = "#FFFFFF";
exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
// 保存原始状态
const originalScale = renderer.scale;
const originalOffsetX = renderer.offsetX;
const originalOffsetY = renderer.offsetY;
const originalCtx = renderer.ctx;
renderer.scale = 1;
renderer.offsetX = -minX + padding_left;
renderer.offsetY = -minY + padding_top;
renderer.ctx = exportCtx;
// 执行渲染
renderer.render();
// 二次填充边缘透明区域
exportCtx.globalCompositeOperation = "destination-over";
exportCtx.fillStyle = "#FFFFFF";
exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
// 导出为JPG
const link = document.createElement("a");
link.download = "mindmap.jpg";
link.href = exportCanvas.toDataURL("image/jpeg", 1.0);
link.click();
}
// 绑定导出事件
document.getElementById('exportBtn').addEventListener('click', exportHighResImage);
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
</script>
</body>
</html>