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

基于excalidraw定制个性化中文手写字体:从零到部署的完整指南

1. 为什么你需要一个专属的中文手写画板?

你是不是也遇到过这样的场景?在线上开会、做笔记或者头脑风暴时,想随手画个草图、写点想法,却发现那些在线白板工具要么字体太“正经”,要么对中文支持不友好,写出来的字方方正正,完全没有手写的那种随性和温度。我之前用Excalidraw就特别喜欢它的英文手写字体Virgil,笔画自然流畅,但一换到中文,瞬间就变回了规规矩矩的宋体或黑体,感觉整个画面的风格都被割裂了,特别别扭。

后来我琢磨,既然Excalidraw是开源的,我们能不能自己动手,给它“喂”一个好看的中文手写字体呢?说干就干,我花了些时间研究,从下载字体、修改代码到最终部署上线,完整地走通了一遍。现在,我已经有了一个完全属于我自己的、带着我喜欢的字体的中文手写画板,用起来别提多顺手了。这个过程其实没有想象中那么复杂,只要你有一点前端开发的基础(知道怎么运行Node.js项目、会用Git),跟着我的步骤走,完全可以从零开始搭建一个。

这个定制版画板能做什么?简单说,它保留了Excalidraw所有强大的绘图功能——画箭头、方框、圆形、自由画笔,插入便签等等,同时让你写的每一个中文字都充满个性。无论是用来做技术架构图、产品原型草图,还是单纯记录灵感,那种笔触带来的亲切感是默认字体无法比拟的。更重要的是,整个过程你完全掌控,字体选你喜欢的,代码部署在你自己的GitHub上,数据安全,用起来也安心。接下来,我就把我从零到一搭建的全过程,包括中间踩过的坑和解决方案,毫无保留地分享给你。

2. 前期准备:打好基础,事半功倍

2.1 获取代码与搭建环境

万事开头难,但第一步其实很简单。我们所有的改造都基于Excalidraw这个优秀的开源项目。首先,你需要访问Excalidraw的GitHub仓库(github.com/excalidraw/excalidraw)。别急着直接克隆,我建议你先点右上角的“Fork”按钮,把代码复制到你自己的GitHub账号下。这样做有两个好处:一是你拥有了一个独立的副本,可以随意修改而不用担心影响原项目;二是方便后续的代码管理和同步更新。

Fork完成后,进入你自己的仓库页面,把项目克隆到本地电脑。打开终端(比如VSCode的终端或者系统的命令行工具),执行类似下面的命令(记得把your-username换成你的GitHub用户名):

git clone git@github.com:your-username/excalidraw.git cd excalidraw

代码拉下来后,我强烈建议你创建一个专门用于开发中文功能的分支。比如,我创建了一个叫zh-dev的分支。这样,你的所有修改都集中在这个分支上,未来如果官方的Excalidraw发布了新版本,你可以很方便地将主分支的更新合并过来,获取新特性,同时保留你的字体定制。

git checkout -b zh-dev

接下来是安装依赖。Excalidraw项目使用yarn作为包管理器。确保你的电脑已经安装了Node.js(建议版本16或以上)和yarn。在项目根目录下运行:

yarn install

这个过程可能会花几分钟,取决于你的网络速度。安装完成后,运行开发服务器:

yarn run start

如果一切顺利,终端会提示服务已经启动。打开浏览器,访问http://localhost:3000,你应该能看到原汁原味的Excalidraw画板了。试试画几笔,感受一下。这个步骤是为了确认你的基础环境是OK的,为后面的“动手术”做好准备。

2.2 挑选一款心仪的中文字体

这是整个过程中最有趣也最个性化的一步——为你未来的画板挑选“外衣”。Excalidraw内置的英文手写体Virgil之所以好看,是因为它模仿了自然手写的笔触和连笔。我们也要为中文寻找具有类似气质的字体。

选择字体时,首要考虑的是版权。务必选择明确标榜可以免费商用,或者遵循SIL Open Font License等宽松开源协议的字体,避免日后产生法律纠纷。我当初找字体时,发现了一个宝藏网站叫“猫啃网”,上面整理了海量免费商用的中文字体,分类清晰,非常方便。你可以根据“手写”、“可爱”、“书法”等标签去筛选。

我最终选择的是“平方雨桐体”。这款字体笔画圆润,带有明显的手写感,又不失清晰度,在Excalidraw的画布上显示效果非常棒。当然,你的选择可能完全不同,或许你喜欢更潦草的行楷,或者更稚嫩的童趣字体,这完全取决于你的审美和用途。

选好字体后,下载它的.ttf.woff2格式文件。为了后续操作方便,我建议将下载的字体文件重命名为一个简单英文名,比如我用的Yutong.ttf。记住这个文件名,后面会反复用到。把字体文件放在一个你容易找到的位置,比如项目的根目录下,我们马上就会用到它。

3. 核心改造:将字体“注入”Excalidraw

现在到了最核心的技术环节:让Excalidraw认识并使用我们新添加的字体。别被“修改代码”吓到,我们不需要理解每一行代码,只需要像拼图一样,在几个关键位置放入我们的“字体模块”。我的方法是,先观察Excalidraw是如何加载它自带的字体(比如Virgil)的,然后依葫芦画瓢。

3.1 放置字体资源文件

首先,需要把字体文件放到项目里正确的位置。Excalidraw的字体资源主要存放在两个地方:

  1. 源码资源目录packages/excalidraw/fonts/assets/。这里存放的是在开发环境下引用的字体文件。
  2. 公共静态资源目录public/。这里存放的是构建后供生产环境使用的字体文件。

所以,我们需要把准备好的Yutong.ttf文件,分别复制到上述两个目录中。你可以直接通过文件管理器拖拽,或者在终端里使用cp命令。确保两个地方都有这个文件。

3.2 注册与声明字体

接下来,我们要在代码中“注册”这个新字体,告诉系统它的存在和属性。

第一步,在CSS中声明字体族。打开文件packages/excalidraw/fonts/assets/fonts.css。你会看到里面已经用@font-face规则声明了Virgil、Cascadia等字体。我们在文件末尾添加对我们字体的声明:

@font-face { font-family: "Yutong"; src: url(./Yutong.ttf) format("truetype"); font-style: normal; font-display: swap; }

这里有几个关键点:font-family是我们给字体起的名字,后面代码里就用这个名字来调用它。src指定了字体文件的路径和格式,注意.ttf格式对应的format"truetype",不是"ttf",这是一个常见的坑,后面我们会再提到。

第二步,在Node.js环境中注册字体(用于服务端渲染或导出)。打开文件packages/excalidraw/index-node.ts。找到一系列registerFont的调用,通常是为Virgil、Cascadia等注册的。我们在后面添加一行:

registerFont("./public/Virgil.woff2", { family: "Virgil" }); registerFont("./public/Cascadia.woff2", { family: "Cascadia" }); // 添加我们自己的字体 registerFont("./public/Yutong.ttf", { family: "Yutong" });

这样,在生成图片或PDF等场景下,我们的字体也能被正确识别。

第三步,预加载字体资源以优化性能。为了让字体更快加载,避免页面渲染后字体才加载导致的闪烁,我们需要添加预加载链接。

  1. 编辑excalidraw-app/index.html,在<head>标签内添加:
    <link rel="preload" href="../packages/excalidraw/fonts/assets/Yutong.ttf" as="font" type="font/ttf" crossorigin="anonymous" />
  2. 编辑scripts/woff2/woff2-vite-plugins.js(这个文件路径可能随版本更新而变化,其作用是处理字体插件),在相应的预加载链接数组里添加一项。你需要找到代码中生成<link rel="preload">的部分,仿照已有的格式添加:
    // 假设在某个函数或数组里 preloadLinks.push(`<link rel="preload" href="/Yutong.ttf" as="font" type="font/ttf" crossorigin="anonymous" />`);

3.3 将字体集成到应用逻辑中

现在字体文件放好了,也声明了,但要让它出现在画板的字体选择下拉框里,并真正被绘制引擎使用,还需要修改几处业务逻辑代码。

首先,给字体分配一个唯一的家族ID。打开packages/excalidraw/constants.ts文件,找到FONT_FAMILY这个常量对象。它里面用键值对的形式定义了所有字体及其对应的数字ID。我们在最后添加一行:

export const FONT_FAMILY = { Virgil: 1, Helvetica: 2, Cascadia: 3, // ... 其他字体 Yutong: 999, // 使用一个较大的数字,避免和未来官方新增字体冲突 };

我习惯把自定义字体的ID设得很大(比如999),这样可以最大程度避免将来合并官方代码时发生ID冲突。

然后,在字体选择器UI中添加选项。打开packages/excalidraw/components/FontPicker/FontPicker.tsx文件。这里定义了字体下拉框中每一个选项。你需要找到渲染选项列表的地方(通常是一个数组的map循环,或者一个静态选项数组)。按照已有选项的格式,添加一个新的选项对象。例如,你可以替换掉一个不常用的内置字体选项(比如原作者的例子中替换了Nunito),或者直接添加一个新项:

// 在选项数组中添加 { value: FONT_FAMILY.Yutong, // 使用上面定义的ID icon: FontFamilyNormalIcon, // 可以先借用现有的图标 text: "雨桐手写", // 显示在下拉框中的文字 testId: "font-family-yutong", // 用于测试的ID }

如果你想让UI更完美,可以专门为这个字体设计或引入一个图标,但初期借用现有图标完全没问题。

最后,也是最关键的一步,在字体加载系统中注册。打开packages/excalidraw/fonts/index.ts文件。这个文件负责管理所有字体的加载。你需要做两件事:

  1. 在文件顶部,和其他字体import语句一起,引入你的字体文件:
    import Yutong from "./assets/Yutong.ttf";
  2. 在文件后面,找到_register函数调用的地方(或者一个字体注册表),添加对你的字体的注册:
    _register("Yutong", FONT_METADATA[FONT_FAMILY.Yutong], { uri: Yutong, });
    注意,这里的FONT_METADATA[FONT_FAMILY.Yutong]可能需要根据实际情况调整。如果FONT_METADATA中没有999这个键,你可以先借用其他字体的元数据(比如FONT_METADATA[FONT_FAMILY.Excalifont]),或者查阅代码看看如何正确定义字体元数据(如字重、样式等)。

完成以上所有步骤后,再次运行yarn run start启动开发服务器。刷新浏览器页面,点击画板上的文本工具,然后在顶部的字体选择下拉框中寻找。你应该能看到你添加的字体选项(比如“雨桐手写”)。选中它,输入一些中文试试看!如果一切顺利,你就能看到文字以你精心挑选的手写字体显示出来了,那一刻的成就感是非常足的。

4. 部署上线:让每个人都能访问你的画板

在本地测试完美之后,我们肯定希望把这个成果分享出去,或者在任何设备上都能使用。最方便、免费的部署方式就是利用GitHub Pages。它会为你的仓库生成一个静态网站,通过https://你的用户名.github.io/仓库名/这样的链接就能访问。

4.1 使用gh-pages自动化部署

首先,我们需要一个工具来帮我们完成将构建好的文件推送到GitHub特定分支的工作。这个工具就是gh-pages。在项目根目录下安装它:

yarn add -D gh-pages

接着,打开package.json文件,在"scripts"部分添加一条部署命令:

"scripts": { "start": "...", "build": "...", "deploy": "gh-pages -d excalidraw-app/build" }

这条命令的意思是:使用gh-pages工具,将excalidraw-app/build目录(这是项目构建后生成的静态文件目录)的内容,推送到远程仓库的gh-pages分支。

4.2 调整构建配置以适应部署环境

在部署前,有一个关键配置需要修改,它决定了浏览器如何加载你的JS、CSS等资源。如果你希望你的画板部署在子路径下(比如username.github.io/excalidraw),就需要设置资源的基础路径。

打开excalidraw-app/vite.config.mts(如果使用Vite)或相应的构建配置文件。找到导出配置的地方,设置base为相对路径'./'

export default defineConfig({ // ... 其他配置 base: './', // 确保资源使用相对路径加载 });

这个设置非常重要,如果不修改,部署后很可能出现页面白屏或资源加载失败的错误。

4.3 执行构建与部署

配置好后,部署就只剩下两条命令了:

# 1. 构建生产环境的静态文件 yarn run build # 2. 部署到GitHub Pages yarn run deploy

执行deploy命令后,gh-pages会自动完成打包、创建或更新gh-pages分支、推送代码等一系列操作。稍等片刻,命令执行成功。

此时,打开你的GitHub仓库,进入Settings -> Pages,你应该能看到GitHub Pages已经自动识别并启用了gh-pages分支作为源。页面上会显示你的站点链接,通常是https://你的用户名.github.io/excalidraw/。点击这个链接,你的专属中文手写画板就正式上线了!

4.4 我踩过的一个大坑:字体格式解析错误

第一次部署成功后,我兴冲冲地打开链接,却发现自定义的字体没有生效,浏览器控制台报了一个错:

DOMException: The source provided ('url(.../Yutong.ttf) format('ttf')') could not be parsed as a value list.

错误信息直指format('ttf')不合法。我明明在fonts.css里写的是format('truetype'),怎么会变成'ttf'呢?通过一番调试追踪,我在packages/excalidraw/fonts/ExcalidrawFont.ts文件中找到了一个名为getFormat的私有方法。这个方法会根据字体文件URL的后缀名自动生成format字符串。问题就出在这里:它简单地将.ttf的后缀ttf直接塞进了format('ttf'),而浏览器并不识别'ttf'这种格式。

解决方案就是修改这个getFormat方法,将ttf映射到正确的truetype。以下是修改后的方法示例:

private static getFormat(url: URL) { try { const pathname = new URL(url).pathname; const parts = pathname.split("."); if (parts.length === 1) { return ""; } // 获取文件扩展名 let extension = parts.pop(); // 关键修复:将 ttf 转换为 truetype if (extension === 'ttf') { extension = 'truetype'; } // 其他格式如 woff2, woff, otf 浏览器能直接识别 return `format('${extension}')`; } catch (error) { return ""; } }

修改完这个方法后,记得重新执行yarn run buildyarn run deploy,再次部署。刷新你的线上页面,这次中文手写字体应该就能正确加载并显示了。这个坑让我深刻体会到,开源项目虽然强大,但在处理一些边缘情况时,还是需要我们自己动手适配。

http://www.jsqmd.com/news/467325/

相关文章:

  • 一篇吃透数据库关系代数,附经典查询案例
  • 避坑指南:QT中QByteArray进制转换时容易踩的3个内存陷阱(带调试技巧)
  • 洛谷刷题必备:5种排序算法实战解析(附竞赛真题代码)
  • Docker容器化部署爬虫项目全流程
  • Isograph Reliability Workbench 14实战:从零构建故障树分析模型
  • SpringBoot与Druid连接池:精准配置PGSQL超时参数避免查询中断
  • vxe-table可编辑行实战:如何避免点击其他行时自动保存当前编辑数据
  • SQL注入实战:堆叠注入的攻防博弈
  • STM32F4 DSP浮点运算实战:从库配置到FFT频谱分析
  • uniapp开发踩坑记:Host version与binary version不匹配的终极解决方案(附详细步骤)
  • RVC功能全体验:语音转换、翻唱制作、实时变声,一个工具全搞定
  • PCIe DMA链表模式实战:如何用LL元素优化数据传输(附PCS-CCS同步避坑指南)
  • 手把手教学:用vllm部署通义千问1.8B-Chat,并用chainlit打造可视化界面
  • ESP32 BLE蓝牙透传实战:从AT指令到数据传输
  • 华为昇腾Atlas 300I Pro推理卡(3010)从零部署:CANN环境配置与ResNet50应用验证
  • 避坑指南:SpringBoot集成DeepSeek API时常见的5个配置错误及解决方法
  • AI辅助开发:让快马AI帮你写出更聪明的Instagram下载工具代码
  • 手把手教你禁用TLS 1.1:Nginx/Apache/Tomcat全平台配置指南(附检测工具)
  • [技术解析] GATv2:从静态到动态,揭秘图注意力网络的真实“注意力”
  • 用OSG+GLSL330实现动态天气效果:从乌云密布到晴空万里的着色器改造指南
  • 绿联NAS+Docker:构建PaddleOCR私有化文档处理流水线
  • Cesium模型单体化避坑指南:从ArcGIS数据准备到分类瓦片生成
  • 超越F1分数:深入解析加权F度量(Fβ)及其在模型评估中的灵活应用
  • Ambari集群部署实战:从零搭建Hadoop管理平台【手把手教程】
  • 网安实战:从Ping命令到RCE漏洞的攻防演练
  • 若依框架-功能探秘_零代码表单构建
  • HR 系统选型避坑:从需求到落地的完整决策框架
  • 单细胞RNA速率分析避坑指南:为什么你的velocyto结果总崩溃?
  • 【技术拆解】从协议到实践:手把手构建你的第一个MCP Server
  • 图解PyG消息传递机制:从GCNConv源码看MessagePassing的5个核心方法