Excalidraw手写体画图工具

1. Excalidraw

一款开源的虚拟手绘风格白板。

产品展示

2. 特点

  • 💯 免费且开源。
  • 🎨 无限的、基于画布的白板。
  • ✍️类似手绘的风格。
  • 🌓 黑暗模式。
  • 🏗️可定制。
  • 📷 图片支持。
  • 😀 形状库支持。
  • 👅 本地化(i18n)支持。
  • 🖼️ 导出为 PNG、SVG 和剪贴板。
  • 💾 开放格式 - 将图纸导出为.excalidrawjson 文件。
  • ⚒️ 多种工具 - 矩形、圆形、菱形、箭头、线条、自由绘制、橡皮擦…
  • ➡️ 箭头绑定和标记箭头。
  • 🔙 撤消/重做。
  • 🔍 缩放和平移支持。

3. 自定义字体-单字体

Excalidraw本身只支持英文的手写体,画图中的中文不是手写体的格式。

image-20240131141046705

所以中文手写体需要二次处理查阅资料,一般有两种方式。一种直接改代码加入字体,自己部署服务,例外一种直接访问官方的画图地址,通过工具拦截修改请求的方式来加载中文字体。各有各的优势,第一种部署自己的服务安全,字体可以任意加多种,用户不需要额外的操作成本,第二种简单,适合自己使用,这里以第一种修改代码的方式

为什么要这样设置单字体

  • 之前写笔记都是用云笔记,后来使用typora,但是都有各种局限性,最近使用找到一个md的编辑器Obsidian,试用后比较好用,准备以后用这个

  • Obsidian有强大的插件功能,其中就有Excalidraw的插件obsidian-excalidraw-plugin,可以自定义字体

    image-20240219193431215

  • 目前这个字体只能设置一个,通过导出文件,查看文件的配置,该字体字体配置的fontFamily字段值为4,所以想要在excalidraw官网和Obsidian插件中自定义中文字体同时生效,就需要设置一样

    image-20240219193823883

3.1 下载字体

经过实际测试比较,发现沐瑶软笔手写体比较好用

image-20240219194338434

3.2 修改源代码

从github拉去源码:https://github.com/excalidraw/excalidraw

涉及修改的文件

  • Muyao-Softbrush.ttf
  • public/fonts/fonts.css
  • packages/excalidraw/constants.ts
  • packages/excalidraw/actions/actionProperties.tsx

实施步骤

  • Muyao-Softbrush.ttf复制字体到public/fonts/

    image-20240219195033150

  • 修改public/fonts/fonts.css

    1
    2
    3
    4
    5
    @font-face {
    font-family: "Muyao";
    src: url("Muyao-Softbrush.ttf");
    font-display: swap;
    }

    image-20240219195129495

  • 修改packages/excalidraw/constants.ts

    1
    2
    3
    4
    5
    6
    7
    export const FONT_FAMILY = {
    Virgil: 1,
    Helvetica: 2,
    Cascadia: 3,
    Assistant: 0,
    Muyao: 4,
    };

    image-20240219195345059

  • 修改packages/excalidraw/actions/actionProperties.tsx

    1
    2
    3
    4
    5
    6
    {
    value: FONT_FAMILY.Muyao,
    text: "沐瑶软笔",
    icon: FreedrawIcon,
    testId: "font-family-virgil",
    },

    image-20240219200219255

4. 自定义字体-多字体

此方法适用于想要设置多个中文自定义手写体

4.1 下载字体

猫啃是一个专注于免费商用字体的网站,包含了几百字免费中文字体,从该网站下载自己喜欢的字体。这里下载两个字体,云峰寒蝉体云峰飞云体

image-20240131142000965

image-20240131142028788

下载完字体后,文件中文名改为英文名称,云峰寒蝉体.ttfg改为YunFengHanChanTi.ttf云峰飞云体.ttf改为YunFengFeiYunTi.ttf

4.2 修改代码

从github拉去源码:https://github.com/excalidraw/excalidraw

涉及修改的文件

  • public/fonts/fonts.css
  • packages/excalidraw/index-node.ts
  • excalidraw-app/index.html
  • packages/excalidraw/constants.ts
  • packages/excalidraw/actions/actionProperties.tsx

具体步骤

  • 复制字体到public/fonts/

image-20240131143146021

  • 修改fonts.css

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @font-face {
    font-family: "YunFengHanChanTi";
    src: url("YunFengHanChanTi.ttf");
    font-display: swap;
    }
    @font-face {
    font-family: "YunFengFeiYunTi";
    src: url("YunFengFeiYunTi.ttf");
    font-display: swap;
    }

    image-20240131143325232

  • 修改index-node.ts

    1
    2
    registerFont("./public/fonts/YunFengHanChanTi.ttf", { family: "YunFengHanChanTi" });
    registerFont("./public/fonts/YunFengFeiYunTi.ttf", { family: "SlidexiaxingRegular" });

    image-20240131143525507

  • 修改index.html,预加载字体,提高启动速度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <link
    rel="preload"
    href="/fonts/YunFengHanChanTi.ttf"
    as="font"
    type="font/ttf"
    crossorigin="anonymous"
    />
    <link
    rel="preload"
    href="/fonts/YunFengFeiYunTi.ttf"
    as="font"
    type="font/ttf"
    crossorigin="anonymous"
    />

    image-20240131143724313

  • 修改constants.ts

    1
    2
    YunFengHanChanTi: 5,
    YunFengFeiYunTi: 6,

    image-20240131143839157

  • 修改actionProperties.tsx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    value: FONT_FAMILY.YunFengHanChanTi,
    text: t("labels.handDrawn"),
    icon: FreedrawIcon,
    testId: "font-family-virgil",
    },
    {
    value: FONT_FAMILY.YunFengFeiYunTi,
    text: t("labels.handDrawn"),
    icon: FreedrawIcon,
    testId: "font-family-virgil",
    },

    image-20240131143957659

5. 启动服务

打开终端,执行以下命令

1
2
3
yarn

yarn start

打开浏览器:http://localhost:3000/

字体上已经多了两个手写体按钮

image-20240131144746526

输入中文

image-20240131144821279

6. 分享

6.1 前提

  • 分享只能通过localhost,127.0.0.1,或者https访问
  • 依赖excalidraw-room服务

6.2 部署excalidraw-room

1
docker run --rm -d --name excalidraw-room -p 9071:80 excalidraw/excalidraw-room:latest

7. Nginx静态文件部署

7.1 修改打包配置

  • 修改package.json
1
2
"build:app": "yarn --cwd ./excalidraw-app build:app",
"build:version": "yarn --cwd ./excalidraw-app build:version",

image-20240205155359728

  • 修改vite.config.mts
1
base: "/excalidraw/",

image-20240205155531489

7.2 打包静态文件

  • 在项目的根目录下执行yarn build
  • 构建完成后,可以看到excalidraw-app目录下生成了一个build文件夹,该文件夹就是编译后的文件

7.3 nginx代理

  • 修改nginx配置文件代理静态文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    worker_processes  1;

    events {
    worker_connections 1024;
    }

    http {
    include mime.types;
    default_type application/octet-stream;

    sendfile on;

    keepalive_timeout 65;

    map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
    }

    server {
    listen 80;
    listen 443 ssl;
    server_name devtool.xxx.com;
    ssl_certificate /home/ssl_cert/xxx.com.pem;
    ssl_certificate_key /home/ssl_cert/xxx.com.key;
    ssl_session_timeout 5m;
    ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    ssl_prefer_server_ciphers on;
    underscores_in_headers on;

    location / {
    root index;
    index index.html index.htm;
    }

    location /excalidraw {
    alias /home/excalidraw/build;
    index index.html;
    }

    location /socket.io {
    proxy_http_version 1.1;
    proxy_pass http://127.0.0.1:9071/socket.io;
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_read_timeout 3600s;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    root html;
    }
    }
    }

7. Docker部署

7.1 构建镜像

为了使用方便,讲代码打包成镜像放在服务器上运行,这样可以多人使用了,当前最新是0.17.0,基于此的Dockerfile代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM node:18 AS build

WORKDIR /opt/node_app

COPY package.json yarn.lock ./
RUN yarn --ignore-optional --network-timeout 600000

ARG NODE_ENV=production

COPY . .
RUN yarn build:app:docker

FROM nginx:1.21-alpine

COPY --from=build /opt/node_app/build /usr/share/nginx/html

HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1

执行build后会报错:

1
2
3
4
5
6
7
8
9
Step 7/10 : RUN yarn build:app:docker
---> Running in e4cb756fd95e
yarn run v1.22.19
$ cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build
/bin/sh: 1: cross-env: not found
error Command failed with exit code 127.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Error response from daemon: The command '/bin/sh -c yarn build:app:docker' returned a non-zero code: 127
Failed to deploy 'excalidraw:aacopy Dockerfile: Dockerfile': Can't retrieve image ID from build stream

测试后发现,Dockerfile文件已经和代码不匹配,无法直接用Dockerfile构建镜像,而且官方提供的镜像包也是很老的版本。

修改如下内容,可以正常打包

  • 修改Dockerfile文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM node:18 AS build

WORKDIR /opt/node_app

COPY package.json yarn.lock ./
COPY excalidraw-app/package.json ./excalidraw-app/
COPY packages/excalidraw/package.json ./packages/excalidraw/
RUN yarn --network-timeout 600000

ARG NODE_ENV=production

COPY . .
RUN yarn build:app:docker

FROM nginx:1.21-alpine

COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html

HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1

  • 修改.dockerignore文件
1
2
3
4
5
6
7
8
9
10
11
12
13
*
!.env.development
!.env.production
!.eslintrc.json
!.npmrc
!.prettierrc
!excalidraw-app/
!package.json
!public/
!packages/
!tsconfig.json
!yarn.lock

  • 修改根目录下的package.json文件
1
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",

image-20240201171205538

此时就可以正常构建镜像了

image-20240201171316252

运行后

image-20240201171346129

7.2 运行镜像

1
docker run -d --name excalidraw -p 9071:80  excalidraw/excalidraw:aacopy

image-20240201171842488

8. 问题

  • centos7受用node启动服务,报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    node:internal/errors:563
    ErrorCaptureStackTrace(err);
    ^

    Error: ENOSPC: System limit for number of file watchers reached, watch '/home/excalidraw/excalidraw/dev-docs/docs/@excalidraw/excalidraw/api/children-components'
    at FSWatcher.<computed> (node:internal/fs/watchers:247:19)
    at Object.watch (node:fs:2473:36)
    at createFsWatchInstance (file:///home/excalidraw/excalidraw/node_modules/vite/dist/node/chunks/dep-9A4-l-43.js:46210:17)
    at setFsWatchListener (file:///home/excalidraw/excalidraw/node_modules/vite/dist/node/chunks/dep-9A4-l-43.js:46257:15)
    at NodeFsHandler._watchWithNodeFs (file:///home/excalidraw/excalidraw/node_modules/vite/dist/node/chunks/dep-9A4-l-43.js:46412:14)
    at NodeFsHandler._handleDir (file:///home/excalidraw/excalidraw/node_modules/vite/dist/node/chunks/dep-9A4-l-43.js:46648:19)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async NodeFsHandler._addToNodeFs (file:///home/excalidraw/excalidraw/node_modules/vite/dist/node/chunks/dep-9A4-l-43.js:46698:16)
    Emitted 'error' event on FSWatcher instance at:
    at FSWatcher._handleError (file:///home/excalidraw/excalidraw/node_modules/vite/dist/node/chunks/dep-9A4-l-43.js:47909:10)
    at NodeFsHandler._addToNodeFs (file:///home/excalidraw/excalidraw/node_modules/vite/dist/node/chunks/dep-9A4-l-43.js:46726:18)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
    errno: -28,
    syscall: 'watch',
    code: 'ENOSPC',
    path: '/home/excalidraw/excalidraw/dev-docs/docs/@excalidraw/excalidraw/api/children-components',
    filename: '/home/excalidraw/excalidraw/dev-docs/docs/@excalidraw/excalidraw/api/children-components'

    • 在CentOS系统上运行yarn和vite时,如果遇到“Error: ENOSPC: System limit for number of file watchers reached”错误,这意味着系统当前设置的文件监视器(file watchers)数量上限已经达到了Linux内核允许的最大值。当Vite、Webpack或类似工具在监听大量文件以实现实时编译和热重载时,可能会触发这个限制。

      1
      2
      3
      4
      5
      6
      7
      8
      # 打开终端并输入以下命令以查看当前的限制:
      cat /proc/sys/fs/inotify/max_user_watches
      # 增加该限制到一个更高的值,例如524288(你可以根据需要选择合适的数值):
      sudo sysctl -w fs.inotify.max_user_watches=524288
      # 为了确保更改在重启后仍然有效,将其添加到/etc/sysctl.conf配置文件中:
      echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf
      # 应用新的配置:
      sudo sysctl -p