当前位置: 首页 > news >正文

Javascript网页设计实例:通过JS实现上传Markdown转化为脑图并下载脑图

功能预览

在这里插入图片描述

深度与密度测试

对于测试部分,分别对深度和密度进行了测试:

注意!!!!!!!只实现了识别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>

相关文章:

  • 火语言RPA--Excel关闭保存文档
  • 【HarmonyOS Next】鸿蒙监听手机按键
  • 汇能感知的光谱相机/模块产品有哪些?
  • 【python】tkinter简要教程
  • oppo,汤臣倍健,康冠科技,高途教育25届春招内推
  • 记录一下windows11编译Openpose的过程
  • 使用VSCODE开发C语言程序
  • 【PLL】应用:时钟生成
  • 【项目日记】仿RabbitMQ实现消息队列 --- 模块设计
  • 【云安全】云原生-Docker(六)Docker API 未授权访问
  • unity学习49:寻路网格链接 offMeshLinks, 以及传送门效果
  • 使用FFmpeg将PCMA格式的WAV文件转换为16K采样率的PCM WAV文件
  • 基于SpringBoot实现的宠物领养系统平台功能一
  • JUC并发编程——Java线程(一)
  • 从线程池到负载均衡:高并发场景下的系统优化实战
  • 本地部署Anything LLM+Ollama+DeepSeek R1打造AI智能知识库教程
  • 【弹性计算】虚拟机云服务器
  • 嵌入式开发:天线(1):天线增益-dBi
  • C/C++子函数申请对应二维数组的动态内存传给主函数使用
  • JavaScript数组-遍历数组
  • 现场丨在胡适施蛰存等手札与文献间,再看百年光华
  • 人民日报:从“轻微免罚”看涉企执法方式转变
  • 夜读丨读《汉书》一得
  • 4月新增社融1.16万亿,还原地方债务置换影响后信贷增速超过8%
  • 古巴外长谴责美国再次将古列为“反恐行动不合作国家”
  • 美国务卿鲁比奥将前往土耳其参加俄乌会谈