多此一举生成器 - 从开发到使用
为什么做这个工具
上次想到这么无厘头的项目还是在上次,今天给大家分享一个非常具有废话文学精神的项目 ——《多此一举生成器》。
想到做这个项目的起因是在跟朋友聊天的时候,经常截图自己发的话来发言,大概就是这样:
每次都要输入一遍并且发送出去才能截图,十分的不方便,所以就想到是否能做一个工具,可以在输入文字后自动生成一张聊天截图。
在地球上,每过 60 秒,就过去了 1 分钟,事不宜迟,让我们马上开始吧。
生成截图
初始化项目
我们将使用 Node.js
来开发这个工具,首先新建工作目录并初始化 npm
项目:
1 | mkdir redundant-tools && cd redundant-tools |
npm
安装 canvas
包,让 Node
支持 canvas
绘图:
1 | npm i canvas |
tips:因为 canvas
包需要根据本地 Node
版本编译原生模块,所以需要 gcc
环境,如果环境不完整可能会安装失败,这时候需要根据 官方( https://www.npmjs.com/package/canvas )指示安装对应环境。
新建 index.js:
1 | const { createCanvas, loadImage } = require('canvas'); |
分析截图构成
一张截图由 背景、头像、气泡、聊天文字 组成,其中背景和气泡颜色会根据 主题(dark/light) 变化。
首先在绘制之前,我们需要知道最终绘制出来的截图尺寸,用于设置 canvas
的宽高。
影响截图宽高最重要的因素就是聊天文字的内容,文字内容多,则宽度更宽,当到达一定界限后,文字内容会换行,换行以后就会影响截图的高度了。
计算高度
截图的最终高度 = 截图内边距(20px * 2) + 气泡内边距(17px * 2) + 行数 * 行高(42px)。
那么怎么知道聊天内容文字的行数呢?
canvas
的 context
提供了一个 measureText
方法,用于测算文字渲染的宽度。入参为文字内容,会返回一个只有 width
的 object
。
tips:字体 family
、size
都将影响最终测算结果。
下面是仿 Mac OS 微信客户端聊天文字样式的文字行数及高度测算:
1 | const chatLineHeight = 42; |
计算宽度
截图最终宽度 = 截图内边距(20px * 2) + 文字内容宽度 + 气泡左内边距(24px) + 气泡右内边距(26px) + 头像大小(64px) + 气泡与头像距离(10px)。
因为我们设置的单行最大宽度为 830px,小于等于 830px 的文字只有一行,超过则为多行,那么可以判断 lines
的 length
,大于 1 的文字宽度取 830px,小于 1 的取 measureText
方法返回的 width
。
1 | const bubbleRightMargin = 10; // 气泡和头像的距离 |
得到真实宽高以后重新设置 canvas
的尺寸:
1 | // 重设 canvas 大小 |
工具函数
这边准备了几个常用的 canvas
绘制工具函数:
1 | // 图片填充模式 |
下面开始绘制截图啦~
背景
绘制一个跟 canvas
一样大的方形,填充主题背景色。
1 | // 绘制底色 |
气泡
原来想的是用安卓 .9.png
的思路,做一个可拉伸的图片来实现气泡,根据文字内容伸缩尺寸,如下图:
然后突然格局打开了,气泡不就是一个圆角矩形加一个三角形么,然后就更换了实现方式。
首先需要知道我们需要画多大的圆角矩形,在哪里画这个圆角矩形。
圆角矩形的尺寸计算跟 canvas
尺寸计算如出一辙:
1 | // 气泡大小 |
绘制的起点则取截图内边距(20px,20px):
1 | // 绘制气泡背景 |
然后给气泡加上箭头,箭头是由 png
图片转换的 base64
数据,使用 canvas
提供的 loadImage
方法加载,需要计算出绘制的位置,代码如下:
1 | const arrowImage = await loadImage(arrowBg); // 加载箭头图片 |
这样我们就得到了一个预留了头像位置的聊天气泡。
头像
头像是可配置的,方便其他同学使用的时候换上自己的头像,所以头像的地址将从运行时的环境变量中获取。然后同样使用 loadImage
加载,在指定位置绘制。
头像的横向起始位置 = 截图总宽度 - 截图内边距(20px) - 头像大小(64px)
头像的纵向起始位置则取截图内边距(20px)
1 | // 头像横向位置 |
这样头像就加好啦,万事俱备,只欠写字。
聊天文字
上面我们已经进行了聊天文字的分行,现在只需要将文字逐行绘制在气泡中,就完事儿了。
1 | // 绘制聊天文字 |
这是绘制一行文字的截图:
这是多行文字的截图:
保存文件
绘制完的图片还在 canvas
中,我们需要将它保存到本地,可以使用 canvas
提供的 toBuffer
方法,将内容输出为二进制流。然后结合 Node
的 fs
模块即可完成保存。
1 | const fs = require('fs'); |
至此,《多此一举生成器》的功能已经开发完成。
便捷使用
让我们尝试在项目根目录下运行:
1 | headUrl=https://fmcat-images.oss-cn-hangzhou.aliyuncs.com/head.png projectPath=. node index.js 这是黑色主题的文字 dark |
这样在根目录下就生成了一张名为 output.png
的图片,在聊天工具中选择图片就能发送了。
但是这样使用起来太繁琐,一个不够便捷的工具不能算一个好工具。
如果能输入聊天内容后直接生成图片,并把图片复制到剪贴板,然后直接在聊天软件粘贴岂不美哉?
为了实现上面的想法,需要引入工作流工具。
工作流
工作中常见的的工作流工具有 Jenkins、阿里云效中的流水线、GitHub Action 等等。配置完成后可以通过特定动作、时间触发流程,完成指定任务,比如项目发布、构建部署、产品输出等等。
这些工具大部分都是在远端环境,无法操作本地环境的剪贴板。
我们将使用一些更便捷,能够和操作系统结合紧密的个人工作流工具来达成目的。
这边仅介绍 Mac OS 下的工具,使用 Windows 的同学可以自行探索。
Alfred
这可以称得上是 Mac OS 上最能提升效率的工具,提供文件、软件搜索比系统自带的更好用,还有剪贴板历史记录、快捷计算器等等便捷功能。
它最强大的功能当属 Workflows,用户可以自己编辑流程,直接支持多种语言,不支持的语言可以通过 bash
运行,开发完的工作流可以导出文件用于分享。社区繁荣,GitHub 上有很多开源的 Workflows 项目可以使用、学习。
比如有道翻译,导入工作流文件以后,配置有道官方申请的 key
和 secret
,使用设置好的快捷键唤起输入框,输入要翻译的文字即可。
心动不如行动,下面我们使用 Alfred Workflows 来配置一个能够便捷使用《多此一举生成器》的工作流。
外部变量
在开发时预留的外部参数分为 运行参数、环境变量。
让我们再来回顾一下运行工具的命令:
1 | headUrl=https://fmcat-images.oss-cn-hangzhou.aliyuncs.com/head.png projectPath=. node index.js 这是黑色主题的文字 dark |
运行参数
chatText:生成截图的聊天文字,定义为命令中 index.js
后面的第一个参数。
theme:截图的主题,将影响背景色和气泡颜色,定义为命令中 index.js
后面的第二个参数。
通过下面的代码获取外部传入的值。
1 | const [chatText = '', theme = 'light'] = process.argv.slice(2); |
环境变量
相对于运行参数,环境变量的值不需要频繁变化,只需要在项目配置时设置一次。
projectPath:项目路径,用于指定 index.js
和截图输出的 output.png
文件位置。
headUrl:设置截图中用户头像的 URL
地址。
新建 Workflows
打开 Alfred 主面板,选择 Workflows 选项卡,新建一个使用关键词触发运行命令的模板。
注意 Bundle Id
需要唯一,后续更新时会判断是否为同一个 Workflows。
右上方可以设置图标,让你的 Workflows 更具辨识度。
设置环境变量
在编辑区的右上方有个 [χ] 按钮,可以打开环境变量编辑面板。
点击右下角的加号按钮依次添加 headUrl
、nodePath
、projectPath
环境变量。
这边需要 nodePath
是因为 Alfred 的默认 bash
环境中找不到 node
命令所在地址,需要我们手动指定一下。
设置关键词
双击 Keyword 流程块打开详情编辑面板。
保存后在 Alfred 输入框中输入 dd
就能看到我们的命令了。
根据主题不同,可以设置两个关键词。
接着在空白区右键,新建一个变量处理块,在下方变量列表中新建一个 theme
,值为 dark
,与 dd 关键词连接。
Argument
中的 {query}
为前方 keyword 块截取的聊天文字。
变量处理块会将内容带给下一个流程块。
同理,theme
值为 light
的变量块与 dl 关键词连接。
判断
项目目录未设置、用户头像未设置都不能顺利运行工具,所以需要提前判断,通知用户进行设置。
在编辑区空白区域右键新建 Conditional 块。
通过 {var:变量名称}
语法取环境变量中的 projectPath
和 headUrl
,判断条件满足会对外暴露一个流程端口,用于连接后续的流程。
在变量为空的接口上连接了一个调用通知的块,可以通过空白区域右键新建,填写对应的通知文本。
现在我们把环境变量中的 projectPath
置空,再次运行工具,将会得到一个提醒设置项目路径的通知。
运行脚本
当 projectPath
和 headUrl
都非空时,会走 else
的端口,后面再连接一个对 nodePath
的判断,两个端口连接的都是 Run Script 块,用于运行代码。
下面对比一下两者有什么区别:
当 nodePath
为空时,会先执行 source ~/.zshrc
,因为目前 Mac OS 默认的终端是 zsh
,且假设使用工具的人都是同行,配置了 nvm
。如果有不同环境,请自行配置。
当 nodePath
非空时,执行代码如下:
手动配置 nodePath
后运行速度会比自动引入更快,这边还是建议手动配置一下。
然后就是跟手动运行项目一样的命令,只是命令中的部分内容被替换成了前面配置的变量。
取环境变量为 $
开头,后面跟环境变量名称,此处的 $1
为上一个块传进来的 query
。
更进一步
完成上面的运行工具就能在项目根目录生成聊天截图了,但是要使用这张图片还是很不方便,需要到项目目录下复制。
能不能把生成的图片直接复制到剪贴板呢?答案是可以的,但是实现的过程很曲折。
首先考虑的是能否使用 Node 来完成这步操作,翻阅文档并没有相关的 API,在 npm 上搜索了很久,只找到能操作文字复制到剪贴板的包。
当我要放弃的时候,想到了 AppleScript,这是苹果自家的脚本语言,可以用于调用系统 APP 完成指定功能。
翻阅文档发现 Finder 有相关的 API 可以实现复制文件到剪贴板,实现起来也很简单,只需要知道文件的绝对路径,就可以完成操作。
我们先使用变量处理块把图片的绝对路径拼接好,传给代码运行块
代码运行块中代码如下:
运行时 {query}
会被替换成拼好的图片绝对路径,这句代码的意思是执行 AppleScript,告诉 Finder 将指定图片设置到剪贴板。
代码运行块后面再连接一个通知块,通知用户图片已经生成完成并且复制到剪贴板了。
流程总览如下。
现在我们只需要唤起 Alfred 输入框,输入 dd/dl 聊天文字
,然后回车等待通知生成完成以后就可以在聊天工具中粘贴发送了。
快捷指令
Apple 于 iOS 11 添加了 捷径 App,后改名为快捷指令,用于实现一些流程化操作,大大提高了 iOS 的可玩性。
如今 Mas OS 12 上也支持了快捷指令,我们可以使用快捷指令来配置《多此一举生成器》,跟 Alfred 的配置方式大同小异,只是在变量设置、获取方面有一些不同。
编辑
运行
将编辑好的快捷指令拖动到菜单栏文件夹,在菜单栏中就可以看到快捷指令的图标了。
点击即可运行,首先会弹出一个选择主题的弹窗,选择后点完成。
接着是要求输入聊天内容的弹窗,输入后点击完成,就开始执行工具脚本了。
执行完成会收到通知。
最终生成的聊天截图:
小结
虽然《多此一举生成器》这个项目没有很大的实用性,只是一个无厘头的搞笑项目,但还是有一些技术含量的。
用好工作流工具,能让你事半功倍,可以把多种技术结合起来,把不可能变为可能。不论是自己写小工具还是团队项目开发,都能有用武之地。
目前项目已经开源,附带了配置好的 Alfred Workflows 和 快捷指令链接,朋友们可以开箱即用。项目地址在下面:
1 | https://github.com/RongleCat/fmcat-redundant-generator |
好了朋友们,马上就要涨潮了,下次再见~