找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2143

积分

0

好友

274

主题
发表于 昨天 18:46 | 查看: 9| 回复: 0

✨前言

不知道大家有没有用过 Excalidraw。Excalidraw 是一款开源的手绘风格虚拟白板,在 GitHub 上已经有 113k star,画出来的流程图、时序图、架构图等很有辨识度,大概是这种风格:

Excalidraw 手绘风格示例

最近我偶然了解到,Excalidraw 的绘图原理是通过 JSON 描述元素。那是不是意味着:可以让 AI 生成符合规范的 JSON,从而实现“自动画图”?

基于这个思路,我做了个工具:通过 AI 自动绘制手绘风格图,并且支持实时流式渲染(边生成边画图):

流式生成并实时渲染

还有一个 gif 版(略糊,但能看出流式渲染过程):

流式渲染 gif

移动端效果:

移动端效果 gif

接下来直接进入实现部分。

AI 实现

这里分享两种实现方式:

  • 方式一:交给 AI 实现。你把需求讲清楚,AI 负责干活;你更像“监工”。如果你对结果更在意、对过程没那么执着,这种方式非常省时间。
  • 方式二:手动编码实现。如果你是为了学习原理和细节,更建议自己从零实现一遍。

先看怎么让 AI 来做。关键在于:把需求整理成足够清晰的 prompt,然后直接复制给 AI。

帮我实现一个 AI + Excalidraw 手绘风格绘图工具,具体要求如下:
## 一、功能需求
1. 集成 Excalidraw 画板,参考官方文档:https://docs.excalidraw.com/docs/@excalidraw/excalidraw/integration
2. 通过 AI 对话生成 Excalidraw JSON 格式的图形元素
3. 流式输出 + 实时渲染:每解析到一个完整的 JSON 元素就立即渲染到画布
4. 支持移动端和桌面端布局
## 二、后端要求
1. AI 调用在服务端进行,不暴露 API 密钥
2. 记录每次对话的 IP、User-Agent、对话内容、响应时间等信息
3. 支持灵活切换各家 AI 服务(OpenAI、智谱、阿里百炼等),封装统一的兼容层
## 三、前端要求
1. 对话历史保存在 localStorage,不依赖后端加载
2. 画布数据也保存在 localStorage,刷新不丢失
3. 流式解析 AI 返回的 JSON 元素,处理嵌套和字符串内的花括号
4. 为 AI 生成的元素补全 Excalidraw 必需的默认字段
## 四、代码质量
1. 组件封装:AI 服务层、流式解析器、画布组件、对话组件分离
2. 类型安全:使用 TypeScript,定义完整的类型
3. 可复用:AI 服务封装成可配置的模块
## 五、AI 绘图提示词要点
需要设计一个 System Prompt,包含:
- 输出格式:纯 JSON,禁止代码块,方便流式解析
- 文字处理:中英文宽度计算、形状内文字双向绑定
- 箭头规范:points 相对坐标写法
- 常用颜色:Excalidraw 内置调色板

建议用带 Plan 模式的 AI IDE(例如 Cursor、Trae):先规划,再执行。AI 生成代码后,一般还需要结合你的实际工程做一些调整,但核心逻辑通常没问题

这种方式的优点是快,真的很快;缺点也明显:你可能对具体实现细节掌握得不够牢。

手动实现

如果目标是把思路吃透,建议手动实现一遍。

整体架构

整个工具可以拆成三层:

[用户输入描述]
      ↓
[AI 流式生成 Excalidraw JSON]
      ↓
[流式解析 JSON 元素]
      ↓
[实时渲染到 Excalidraw 画布]

也就是:用户描述想画的图 → AI 生成符合 Excalidraw 规范的 JSON 元素 → 前端实时解析并渲染到画布。

整体流程示意

我这里前端用的是 Next.js ,并集成官方的 @excalidraw/excalidraw 包。后端使用 Vercel AI SDK 对接大模型,它支持 OpenAI 以及兼容服务(智谱、阿里百炼等)。

核心实现

一、Excalidraw JSON 规范

Excalidraw 的绘图原理本质上是:用 JSON 描述元素。

每个元素通常都会有这些基础属性:idtypexywidthheight。不同类型还有各自特有的字段。

支持的常见元素类型如下:

类型 说明 特有属性
rectangle 矩形 -
ellipse 椭圆 -
diamond 菱形 -
text 文本 text, fontSize, fontFamily
arrow 箭头 points, endArrowhead
line 线条 points

一个简单的矩形元素示例:

{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "rect-1",
      "type": "rectangle",
      "x": 100,
      "y": 100,
      "width": 150,
      "height": 80,
      "backgroundColor": "#a5d8ff",
      "strokeColor": "#1971c2"
    }
  ]
}

其中 backgroundColor 是矩形填充色,strokeColor 是边框色:

颜色示例

二、AI 提示词设计

要让 AI 生成“可用的 JSON”,同时保证出图效果,提示词需要覆盖一些关键约束,例如:

  • 输出格式:纯 JSON,不要代码块(便于流式解析)
  • 文字处理:中英文宽度计算、形状内文字双向绑定
  • 箭头规范:points 是相对坐标,不同方向箭头怎么写
  • 常用颜色:Excalidraw 内置调色板的选择范围

我的 prompt 结构大概如下(示意):

你是一个专业的 Excalidraw 绘图助手。用户会描述他们想要绘制的图形、流程图、架构图等,你需要生成对应的 Excalidraw 元素 JSON。
## 输出格式要求
1. **先说明,后输出**:先简要说明要画什么,然后连续输出所有 JSON 元素
2. **禁止**在 JSON 元素之间穿插说明文字
3. 每个元素直接输出纯 JSON 对象,以 { 开头,以 } 结尾
4. **禁止**使用代码块(禁止 ``` 符号)
5. **必须**使用标准 JSON 格式:键值对用冒号分隔(如 "x":100),不要写成等号
## 文字处理
## 箭头和线条
## 元素基础结构
## 基础形状
## 常用颜色

提示词如果写得足够细,后面的流式解析和渲染都会省心很多。反过来,如果 prompt 过于随意,你就会在“解析失败、元素缺字段、画面错乱”里反复踩坑。

三、流式 JSON 解析

AI 返回内容是流式的,需要边接收边解析。核心任务是:从持续增长的文本里,提取出完整的 JSON 对象(一个元素一个对象)。

下面是核心解析思路:通过花括号深度来找到完整对象,同时要正确处理字符串内的 {},避免误判。

// 从流式文本中提取完整的 JSON 对象
function extractJsonObjects(text: string) {
  const results = []
  let i = 0

  while (i < text.length) {
    const startIndex = text.indexOf('{', i)
    if (startIndex === -1) break

    // 追踪花括号深度,处理嵌套
    let depth = 0
    let inString = false

    for (let j = startIndex; j < text.length; j++) {
      // ... 处理转义字符和字符串
      if (char === '{') depth++
      else if (char === '}') {
        depth--
        if (depth === 0) {
          // 找到完整的 JSON 对象
          results.push(text.slice(startIndex, j + 1))
          break
        }
      }
    }
  }
  return results
}

这里至少有三个关键点:

1)用花括号深度处理嵌套 JSON
2)正确处理字符串内的花括号(否则非常容易误判)
3)记录已处理位置,避免重复解析同一段文本

当然,真实场景里你可能还会遇到更多边界情况,按需补齐即可。

四、实时渲染

解析出元素后,通过 Excalidraw API 添加到画布。示例逻辑如下(注意处理 id 冲突):

// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
  addElements: (newElements) => {
    const api = excalidrawAPIRef.current
    const currentElements = api.getSceneElements()

    // 处理 id 冲突
    const elementsToAdd = newElements.map((el) => {
      if (existingIds.has(el.id)) {
        return { ...el, id: generateNewId() }
      }
      return el
    })

    // 更新画布
    api.updateScene({
      elements: [...currentElements, ...elementsToAdd],
    })
  },
}))

到这里,主链路就打通了:AI 流式输出 → 解析 JSON → 立即渲染

为了让这个“玩具”更稳一点,还需要加一层降级策略:默认字段补全。

五、元素默认值补全

AI 可能只生成必要字段,但 Excalidraw 运行时还依赖更多属性。为避免渲染报错或样式异常,可以在入场前补全默认值:

function getDefaultElementProps() {
  return {
    angle: 0,
    strokeColor: '#1e1e1e',
    backgroundColor: 'transparent',
    fillStyle: 'solid',
    strokeWidth: 2,
    roughness: 1, // 手绘粗糙度
    opacity: 100,
    seed: Math.random() * 100000, // 随机种子,产生手绘效果
    version: 1,
    versionNonce: Math.random() * 1000000000,
    isDeleted: false,
    groupIds: [],
    boundElements: null,
  }
}

这一步本质上是一种“兜底”:即使 AI 少给了字段,也尽量保证画布不会崩。

移动端适配

为了在手机上也能使用,我做了移动端适配。核心目标只有一个:能输入描述、能看到出图结果。至于编辑、拖拽等精细操作,移动端就先不追求了。

移动端采用上下布局:上方画布 + 底部输入框:

移动端布局

数据持久化

这个工具的画布数据和对话历史都保存在 localStorage 里,刷新也不会丢失,同时不依赖后端加载。

如果你也在做类似“前端状态本地持久化”的功能,可以在 云栈社区 找到更多工程化实践与讨论。

🎯结语

AI + Excalidraw 做手绘风格绘图工具,核心就两件事:

  • 让 AI 输出符合规范的 JSON 元素
  • 前端流式解析并实时渲染到画布

至于效果,我只能说“流程是通的,效果是拉胯的”,哈哈哈。当前阶段更适合作为玩具尝鲜,距离生产可用还有不少优化空间。

体验地址: https://www.lzkz.top/tool/excalidraw




上一篇:C++避坑:std::vector<bool>代理对象缺陷与替代方案
下一篇:WebClient协议如何选:HTTP/2与连接池调优
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 17:10 , Processed in 0.207749 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表