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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/972702.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

什么是tomcat

什么是tomcat Tomcat 是由Apache软件基金会开发的一个开源的轻量级Web服务器&#xff0c;主要用于部署和运行Java Servlet和JavaServer Pages (JSP) 技术构建的Web应用。以下是关于Tomcat的一些关键点&#xff1a; Java Web应用容器&#xff1a;Tomcat作为Servlet容器&#x…

一.AI大模型开发-初识机器学习

机器学习基本概念 前言 本文主要介绍了深度学习基础&#xff0c;包括机器学习、深度学习的概念&#xff0c;机器学习的两种典型任务分类任务和回归任务&#xff0c;机器学习中的基础名词解释以及模型训练的基本流程等。 一.认识机器学习 1.人工智能和机器学习 人工智能&am…

Redis未授权访问漏洞原理

redis未授权访问漏洞 目录 redis未授权访问漏洞一、Redis介绍二、redis环境安装三、漏洞原理四、漏洞复现4.1 webshell提权4.2redis写入计划任务反弹shell4.3 ssh key免密登录4.4 Redis基于主从复制的RCE方式 五、Redis加固建议 一、Redis介绍 Redis&#xff0c;全称为Remote …

什么是网络安全?网络安全防范技术包括哪些?

伴随着互联网的发展&#xff0c;它已经成为我们生活中不可或缺的存在&#xff0c;无论是个人还是企业&#xff0c;都离不开互联网。正因为互联网得到了重视&#xff0c;网络安全问题也随之加剧&#xff0c;给我们的信息安全造成严重威胁&#xff0c;而想要有效规避这些风险&…

使用VSCODE开发C语言程序

使用vscode配置C语言开发环境 一、安装VSCODE 1、下载vscode ​ 从官方网站&#xff08;https://code.visualstudio.com/Download&#xff09;上&#xff0c;下载windows版本的vscode 2、安装vscode ​ 下载完毕后&#xff0c;按照提示进行安装即可&#xff08;尽可能不要安…

轴承故障特征—SHAP 模型 3D 可视化

往期精彩内容&#xff1a; Python-凯斯西储大学&#xff08;CWRU&#xff09;轴承数据解读与分类处理 基于FFT CNN - BiGRU-Attention 时域、频域特征注意力融合的轴承故障识别模型-CSDN博客 基于FFT CNN - Transformer 时域、频域特征融合的轴承故障识别模型-CSDN博客 P…

ComfyUI多功能插件安装-Comfy UI Manager

原生ComfyUI中的节点内容较少&#xff0c;在使用过程中所需要的很多实用插件没有 可以安装 Comfy UI Manager 以帮助我们更高效率的使用ComfyUI&#xff08;Comfy UI Manager 也相当于一个节点&#xff0c;但是可以管理、更新其他实用节点&#xff09; ComfyUI是由Dr.Lt.Data开…

PET-SQL:基于大模型的两阶段Text2SQL方法

一. 背景 论文: PET-SQL: A Prompt-Enhanced Two-Round Refinement of Text-to-SQL with Cross-consistency 二. 算法介绍 2.1 主要流程 PET-SQL这篇文章提出了一种创新的基于大型语言模型的Text2SQL方法&#xff0c;该方法将SQL生成过程分为两个不同的阶段。在第一阶段&am…

大模型开发实战篇4:多模态之图片识别

多模态的定义 在人工智能领域&#xff0c;模态指的是数据的不同形式或类型&#xff0c;例如文本、图像、音频、视频等。多模态则意味着模型能够处理和理解多种不同模态的数据。 大模型的多模态指的是那些能够处理和理解多种模态数据的大型人工智能模型。这些模型通常基于深度…

力扣 跳跃游戏 II

贪心算法&#xff0c;存下每一步的最远&#xff0c;去达到全局的最小跳跃次数。 题目 从题中要达到最少次数&#xff0c;肯定是每一步尽可能走远一点。但注意j被限制了范围&#xff0c;这种不用想每一步遍历时肯定选最大的num[i]&#xff0c;但要注意&#xff0c;题中是可以到…

goland无法debug项目

1、其实个原因是因为正在使用的Delve调试器版本太旧&#xff0c;无法兼容当前的Go语言版本1.2。Delve是Go语言的一个调试工具&#xff0c;用于提供源码级别的调试功能。Go语言每隔一段时间会发布新版本&#xff0c;而相应的调试器Delve也可能会更新以提供新的特性或修复已知问题…

SpringAI系列 - ToolCalling篇(二) - 如何设置应用侧工具参数ToolContext(有坑)

目录 一、引言二、集成ToolContext示例步骤1: 在`@Tool`标注的工具方法中集成`ToolConext`参数步骤2:`ChatClient`运行时动态设置`ToolContext`参数三、填坑一、引言 在使用AI大模型的工具调用机制时,工具参数都是由大模型解析用户输入上下文获取的,由大模型提供参数给本地…

​实在智能与宇树科技、云深科技一同获评浙江省“人工智能服务商”、 “数智优品”​等荣誉

近日&#xff0c;浙江省经信厅正式公布《2024 年浙江省人工智能应用场景、应用标杆企业、人工智能服务商及 “数智优品” 名单》。 实在智能获评浙江省“人工智能服务商”&#xff0c;核心产品 “实在 Agent 智能体” 入选 “数智优品”。一同获此殊荣的还有宇树科技、云深处科…

【云安全】云原生-Docker(六)Docker API 未授权访问

Docker API 未授权访问 是一个非常严重的安全漏洞&#xff0c;可能导致严重的安全风险。 什么是 Docker API &#xff1f; Docker API 是 Docker 容器平台提供的一组 RESTful API&#xff0c;用于与 Docker 守护程序进行通信和管理 Docker 容器。通过 Docker API&#xff0c;…

open-webui安装

docker安装openwebui 拉取镜像 docker pull ghcr.io/open-webui/open-webui:maindocker images启动 docker run -d -p 8346:8080 --name open-webui ghcr.io/open-webui/open-webui:maindocker ps查看端口占用 lsof -i:8346访问地址 http://ip:port http://127.0.0.1:8346

在ubuntu上用Python的openpyxl模块操作Excel的案例

文章目录 安装模块读取Excel数据库取数匹配数据和更新Excel数据 在Ubuntu系统的环境下基本职能借助Python的openpyxl模块实现对Excel数据的操作。 安装模块 本次需要用到的模块需要提前安装(如果没有的话) pip3 install openpyxl pip3 install pymysql在操作前&#xff0c;需…

SOME/IP--协议英文原文讲解8

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 4.2 Speci…

基于YOLO11深度学习的果园苹果检测与计数系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…

【C++】 Flow of Control

《C程序设计基础教程》——刘厚泉&#xff0c;李政伟&#xff0c;二零一三年九月版&#xff0c;学习笔记 文章目录 1、选择结构1.1、if 语句1.2、嵌套的 if 语句1.3、条件运算符 ?:1.4、switch 语句 2、循环结构2.1、while 语句2.2、do-while 语句2.3、 for 循环2.4、循环嵌套…

mysql 学习15 SQL优化,插入数据优化,主键优化,order by优化,group by 优化,limit 优化,count 优化,update 优化

插入数据优化&#xff0c; insert 优化&#xff0c; 批量插入&#xff08;一次不超过1000条&#xff09; 手动提交事务 主键顺序插入 load 从本地一次插入大批量数据&#xff0c; 登陆时 mysql --local-infile -u root -p load data local infile /root/sql1.log into table tb…