使用excalidraw搭建自己的中文手写画板

成品预览地址https://guizimo.github.io/excalidraw/

excalidraw提供了英文的手写体,但中文还是正正方方的,感觉不搭。希望中文也可以有那样一种手写风格。

效果预览

本文使用的是excalidraw,它是一个十分优秀的开源项目,地址:https://github.com/excalidraw/excalidraw

1、创建项目

1.1、拉取代码

首先在excalidrawGithub主页Fork一份代码到自己的账户下

excalidraw

Fork之后,clone到本地

 git clone git@github.com:guizimo/excalidraw.git

然后在这里,签出一个新的分支:zh-dev,这个分支用来提交我们自定义的提交和变动,在后续如果excalidraw更新后,可以合并主干,得到最新的特性。

1.2、运行

尝试本地运行

// 安装依赖
yarn install
// 运行
yarn run start

在浏览器中打开http://localhost:3000/,发现已经可以正常运行了

预览

2、下载中文字体

选择一款你看的舒服的中文手写字体,当然这里你喜欢其他的字体也可。需要注意的是字体的版权问题哈。笔者在选择中文字体的时候发现了一个网站:猫啃网,大家有空可以自己去了解哈。

我这边选择的是平方雨桐体,在这里感谢作者提供那么好的字体!

平方雨桐体预览

下载好中文字体包,并重命名为 Yutong.ttf

3、加入中文字体

到这一步了需要手动调整代码了,因为不同的版本,代码会有一些调整。目前的excalidraw使用了monorepo来管理项目了。

这里提供一种思路,观察其中的一个内置的字体,通过断点调试了解它的加载流程,按照它的思路,添加我们自定义的字体即可。

3.1、放置字体资源

将我们之前下载好的字体文件 Yutong.ttf放置到 packages/excalidraw/fonts/assetspublic目录下。

3.2、注册字体

编辑 packages/excalidraw/fonts/assets/fonts.css,添加中文手写体Yutong

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

编辑packages/excalidraw/index-node.ts添加registerFont

registerFont("./public/Virgil.woff2", { family: "Virgil" });
registerFont("./public/Yutong.ff2", { family: "Yutong" });
registerFont("./public/Cascadia.woff2", { family: "Cascadia" });

3.3、预加载字体资源

编辑excalidraw-app/index.html,添加一个link

<link
   rel="preload"
   href="../packages/excalidraw/fonts/assets/Yutong.ttf"
   as="font"
   type="font/ttf"
   crossorigin="anonymous"
/>

编辑scripts/woff2/woff2-vite-plugins.js,添加一个link

<link
  rel="preload"
  href="/Yutong.ttf"
  as="font"
  type="font/ttf"
  crossorigin="anonymous"
/>

编辑 packages/excalidraw/constants.ts,在 FONT_FAMILY 常量中加入字体的枚举。

这里可以把枚举的数字设置大一点,防止后续更新新加了字体,导致冲突。

export const FONT_FAMILY = {
  Virgil: 1,
  Helvetica: 2,
  Cascadia: 3,
  // leave 4 unused as it was historically used for Assistant (which we don't use anymore) or custom font (Obsidian)
  Excalifont: 5,
  Nunito: 6,
  "Lilita One": 7,
  "Comic Shanns": 8,
  "Liberation Sans": 9,
  Yutong: 999
}

编辑packages/excalidraw/components/FontPicker/FontPicker.tsx,在列表中添加切换按钮。

这里懒得去添加图标和枚举了,我把Nunito的位置给占了哈,不像我这边懒的人可以去加上图标哈。

  {
    value: FONT_FAMILY.Yutong,
    icon: FontFamilyNormalIcon,
    text: t("labels.normal"),
    testId: "font-family-yutong",
  },
  // {
  //   value: FONT_FAMILY.Nunito,
  //   icon: FontFamilyNormalIcon,
  //   text: t("labels.normal"),
  //   testId: "font-family-normal",
  // },

编辑packages/excalidraw/fonts/index.ts添加_register

import Yutong from "./assets/Yutong.ttf";

......
_register("Yutong", FONT_METADATA[FONT_FAMILY.Excalifont], {
  uri: Yutong,
});

再次运行起来,那么在使用字体的第二项的时候,已经可以正常使用我们刚添加的中文手写字体了。

测试

4、部署

在本地已经可以正常使用了,为了更加方便使用,这边直接选择使用Github Page部署。可以借助gh-pages来实现。

gh-pages的原理

就是创建一个gh-pages的分支,将选中的已经构建好的资源文件,放入该分支,然后推送到远程的Github仓库,Github识别到gh-pages的分支有更新,会生成一个deploy,通过该链接就可以正常访问了。

4.1、安装依赖

yarn add -D gh-pages

修改package.json文件,添加deploy脚本。

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

4.2、调整配置

注意:目前笔者是使用二级部署的,即部署之后访问的链接形式是:https://xxx.com/xxx/excalidraw,需要调整打包之后的资源引用路径。

编辑excalidraw-app/vite.config.mts,使用相对路径base: './'

export default defineConfig({
  ......,
  base: './', // 使用相对路径
  ......,
})

4.3、构建部署

首先打包成为静态资源

yarn run build

部署

yarn run deploy

执行之后,在远程仓库中,会自动创建github-pages deployments

deployments

这里会给到一个链接:https://guizimo.github.io/excalidraw/

image-20240812181447558

打开链接就可以看到成功部署的中文手写版excalidraw了。

image-20240808141942539

编辑Github主页配置。

image-20240812182014787

4.4、踩坑

部署成功之后,自己添加的字体无法正常显示。

报错信息

DOMException: The source provided ('url(https://guizimo.github.io/excalidraw/assets/Yutong-BrR4G41P.ttf) format('ttf')') could not be parsed as a value list.

发现format('ttf')并不是一个合法的格式。但是搜索整个代码,并未声明format('ttf')这样的一种格式。

通过断点调试找到了在packages/excalidraw/fonts/ExcalidrawFont.tsgetFormat方法中,会将字体文件的后缀名作为format,导致浏览器加载不上我们添加的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 "";
    }
    // ttf is not a valid format, so we are converting it to truetype
    let type = parts.pop()
    if (type === 'ttf') {
      type = 'truetype'
    }
    return `format('${type}')`;
  } catch (error) {
    return "";
  }
}

各种字体对应的格式

  • format('truetype'):用于指定 TrueType 字体(.ttf 文件)的格式。
  • format('woff'):用于指定 Web Open Font Format 字体(.woff 文件)的格式。
  • format('woff2'):用于指定 Web Open Font Format 2 字体(.woff2 文件)的格式。
  • format('opentype'):用于指定 OpenType 字体(.otf 文件)的格式。

重新构建部署,自定义的字体已经生效!

效果预览