想到小程序中有如此大量的生成图片需求,而 Canvas 生成方法又是如此难用和坑爹(有关小程序的坑,可看 https://github.com/Kujiale-Mobile/MP-Keng )。我们就想到可不可以做一款可以很方便生成图片,并且还能屏蔽掉直接使用 Canvas 的一些坑的库呢?对此我们发起了 “画家计划— 通过 json 数据形式,来进行动态渲染并绘制出图片”。 Painter 库的整体架构如下:

首先,我们定义了一套绘图 JSON 规范,开发者可以根据需求构建生成图片的 Palette(调色板),然后在程序运行过程中把调色板传入给 Painter(画家)。Painter 会调用 Pen(画笔),根据 Palette 内容绘制出对应的图片后返回。
经过了一段时间的进步,painter 在大家的建议与贡献下得到了长足的成长。我们感谢各位使用者在这个过程中对 painter 的支持和帮助,这也是我们不断完善 painter 的最大动力。我们将为大家介绍 painter 的新能力,并明确下一阶段的迭代目标。
Painter 的优势
TODO
painter 的 “canvas2d 版本”与“base64 支持”正处于测试状态,可以在上方测试版本处链接获取对应版本,欢迎各位在实际体验后向我们反馈存在的问题,并给出宝贵的改进经验。你的支持将帮助 painter 做的更好
git clone https://github.com/Kujiale-Mobile/Painter.git
代码下载后,用小程序 IDE 打开后即可使用。
注:请选择小程序项目,非小游戏,例子中无 appid,所以无法在手机上运行,如果需要真机调试,请在打开例子时,填上自己的小程序 id
mpvue 的使用方法请移步 mpvue 接入方案
taro 的使用方法请参考 Taro 接入方案 painter 已发布 taro 版本的 npm 包mina-painter
Painter 的核心代码在另一个 repo 中,https://github.com/Kujiale-Mobile/PainterCore.git 。你可以通过 submodule 的方式进行库的引入。有关 submodule 的用法可自行 Google。
powershell
git submodule add https://github.com/Kujiale-Mobile/PainterCore.git components/painter
json
"usingComponents":{
"painter":"/components/painter/painter"
}
palette 字段作为画图数据的数据源, 图案数据以 json 形式存在,推荐使用“皮肤模板”的方法进行传递,示例代码如下:xml
<painter palette="{{data}}" bind:imgOK="onImgOK" />
你可以通过设置 widthPixels 来强制指定生成的图片的像素宽度,否则,会根据你画布中设置的大小来动态调节,比如你用了 rpx,则在 iphone 6 上会生成 0.5 倍像素的图片。由于 canvas 绘制的图片像素直接由 Canvas 本身大小决定,此处通过同比例放大整个画布来实现对最后生成的图片大小的调节。
xml
<painter customStyle='position: absolute; left: -9999rpx;' palette="{{template}}" bind:imgOK="onImgOK" widthPixels="1000"/>
```javascript bind:imgOK="onImgOK" bind:imgErr="onImgErr"
onImgOK(e) { 其中 e.detail.path 为生成的图片路径 }, ```
dancePalette 、 action 等字段开启 painter 的高阶用法。具体使用方式将会在下方有详细描述。在新版 painter 中,静态模版默认相对 painter 本身 left: -9999px 。因此正常情况下使用 painter 时出现在页面上的都是动态模版。如果希望禁止用户的操作,可以按照使用静态模版的做法,只传 palette 属性即可。| 属性 | 类型 | 说明 | 必填 | 默认值 |
|---|---|---|---|---|
| customStyle | string | canvas 的自定义样式 | 否 | |
| palette | IPalette | 静态模版,具体规范下文有详细介绍 | 否 | |
| scaleRatio | number | 缩放比,会在传入的 palette 中统一乘以该缩放比,作用和 widthPixels 类似,所以不要同时使用 | 否 | 1 |
| widthPixels | number | 生成的图片的像素宽度,如不传则根据模版动态生成 | 否 | |
| dirty | boolean | 是否启用脏检查 | 否 | false |
| LRU | boolean | 是否开启 LRU 机制 | 否 | false |
| dancePalette | IPalette | 动态模版,规范同静态模版 | 否 | |
| customActionStyle | ICustomActionStyle | 选择框、缩放图标、删除图标的自定义样式与图片 | 否 | |
| action | IView | 动态编辑内容,用于刷新动态模版 | 否 | |
| disableAction | boolean | 禁止动态编辑操作 | 否 | false |
| clearActionBox | boolean | 清除动态编辑框 | 否 | false |
| imgErr | function | 图片生成失败,可以从 e.detail.error 获取错误信息 | 否 | |
| imgOk | function | 图片生成成功,可以从 e.detail.path 获取生成的图片路径 | 否 | |
| viewUpdate | function | 动态模版, view 被更新,可从 e.detail.view 获取更新的 view | 否 | |
| viewClicked | function | 动态模版, view 被选中, 可从 e.detail.view 获取点击的 view,如为空,则是选中背景 | 否 | |
| touchEnd | function | 动态模版,触碰结束。只有 view,代表触碰的对象;包含 view、type、index,代表点击了删除 icon; | 否 | |
| didShow | function | 动态模版,绘制结束时触发 | 否 | |
| use2D | boolean | 是否使用 canvas2d 接口(注意!使用 use2D 就无法使用 dancePalette 与 action) | 否 | false |
interface IView {
type: "rect" | "text" | "image" | "qrcode";
text?: string;
url?: string;
id?: string;
/** 事实上painter中view的css属性并不完全与CSSProperties一致。 */
/** 有一些属性painter并不支持,而当你需要开启一些“高级”能力时,属性的使用方式也与css规范不一致。 */
/** 具体的区别我们将在下方对应的view介绍中详细讲解,在这里使用CSSProperties仅仅是为了让你享受代码提示 */
css: CSSProperties;
}
interface IPalette {
background: string; // 整个模版的背景,支持网络图片的链接、纯色和渐变色
width: string;
height: string;
borderRadius: string;
views: Array<IView>;
}
interface ICustomActionStyle {
border: string; // 动态编辑选择框的边框样式
scale: {
textIcon: string; // 文字view所使用的缩放图标图片
imageIcon: string; // 图片view所使用的缩放图标图片
};
delete: {
icon: string; // 删除图标图片
};
}
如你使用 wxss + wxml 规范进行绘制一样,Painter 需要根据一定的规范来进行图片绘制。当然 Painter 的绘制规范要比 wxml 简单很多。这部分的例子都是基于 palette 属性实现的静态模版
一个调色板首先需要给予一些整体属性
background: 可以是颜色值,也可以为网络图片的链接,默认为白色,支持渐变色
width: 宽度
height: 高度
borderRadius: 边框的圆角(该属性也同样适用于子 view)
views: 里面承载子 view
当我们把整体的调色板属性构建起来后,里面就可以添加子 View 来进行绘制了。
| type | 内容 | description | 自有 css |
|---|---|---|---|
| image | url | 表示图片资源的地址,本地或网络 | 见 image 小节 |
| text | text | 文本的内容 | 见 text 小节 |
| rect | 无 | 矩形 | color: 颜色,支持渐变色 |
| qrcode | content | 画二维码 | background: 背景颜色(默认为透明色)color: 二维码颜色(默认黑色) |
Painter 的 image 可以设置成本地图片或者网络图片,注意本地图片请使用绝对路径。并且如果未设置 image 的长宽,则长宽的属性值会默认设为 auto。若长宽均为 auto 则会使用图片本身的长宽来布局,大小为图片的像素值除以 pixelRatio 。
| 属性名称 | 说明 | 默认值 |
|---|---|---|
| width | image 的宽度 | auto |
| height | image 的高度 | auto |
| mode | 图片裁剪、缩放的模式 | aspectFill |
scaleToFill:不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
aspectFill:保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
注:mode 属性和小程序 image 的 mode 属性功能一致,只是支持的类型只有两种,且默认值不同。 当 width 或 height 属性设置为 auto 时,mode 属性失效

例子代码(点击展开)
export default class ImageExample {
palette() {
return {
width: "654rpx",
height: "1000rpx",
background: "#eee",
views: [
{
type: "image",
url: "/palette/sky.jpg",
},
{
type: "text",
text: "未设置height、width时",
css: {
right: "0rpx",
top: "60rpx",
fontSize: "30rpx",
},
},
{
type: "image",
url: "/palette/sky.jpg",
css: {
width: "200rpx",
height: "200rpx",
top: "230rpx",
},
},
{
type: "text",
text: "mode: 'aspectFill' 或 无",
css: {
left: "210rpx",
fontSize: "30rpx",
top: "290rpx",
},
},
{
type: "image",
url: "/palette/sky.jpg",
css: {
width: "200rpx",
height: "200rpx",
mode: "scaleToFill",
top: "500rpx",
},
},
{
type: "text",
text: "mode: 'scaleToFill'",
css: {
left: "210rpx",
top: "560rpx",
fontSize: "30rpx",
},
},
{
type: "image",
url: "/palette/sky.jpg",
css: {
width: "200rpx",
height: "auto",
top: "750rpx",
},
},
{
type: "text",
text: "设置height为auto",
css: {
left: "210rpx",
top: "780rpx",
fontSize: "30rpx",
},
},
],
};
}
}
因为 text 的特殊性,此处对 text 进行单独说明。
| 属性名称 | 说明 | 默认值 |
|---|---|---|
| width | text 的宽度 | |
| height | text 的高度 | |
| fontSize | 字体大小 | 20rpx |
| color | 字体颜色 | black |
| maxLines | 最大行数 | 不限,根据 width 来 |
| lineHeight | 行高(上下两行文字 baseline 的距离) | fontSize 大小 |
| fontWeight | 字体粗细。 | normal |
| textDecoration | 文本修饰,支持 underline、 overline、 line-through,也可组合使用 | 无效果 |
| textStyle | fill: 填充样式,stroke:镂空样式 | fill |
| background | 文字背景颜色 | 无 |
| padding | 文字背景颜色边际与文字间距 | 0rpx |
| textAlign | 文字的对齐方式,分为 left, center, right,view 的对齐方式请看 align 属性 | left |
当文字设置 width 属性后,则文字布局的最大宽度不会超过该 width 。如果内容超过 width,则会进行换行,如果此时未设置 maxLines 属性,则会把所有内容进行换行处理,行数由内容和 width 决定。如果此时设置了 maxLines 属性,则最大展示所设置的行数,如果还有多余内容未展示出来,则后面会带上 ... 。
关于 fontFamily 属性,有一点需要澄清,最开始文档中写的可以通过 wx.loadFontFace 来加载自定义字体,是不严谨的。事实上,原版 canvas 接口不支持自定义字体。而从 2.13.0 版本基础库开始,canvas2d 版本的接口开始支持自定义字体。我们找到了如下问答作为依据: 问题链接。

例子代码(点击展开)
export default class LastMayday {
palette() {
return {
width: "654rpx",
height: "700rpx",
background: "#eee",
views: [
_textDecoration("overline", 0),
_textDecoration("underline", 1),
_textDecoration("line-through", 2),
_textDecoration("overline underline line-through", 3, "red"),
{
type: "text",
text: "fontWeight: 'bold'",
css: [
{
top: `${startTop + 4 * gapSize}rpx`,
fontWeight: "bold",
},
common,
],
},
{
type: "text",
text: "我是把width设置为300rpx后,我就换行了",
css: [
{
top: `${startTop + 5 * gapSize}rpx`,
width: "400rpx",
},
common,
],
},
{
type: "text",
text: "我设置了maxLines为1,看看会产生什么效果",
css: [
{
top: `${startTop + 7 * gapSize}rpx`,
width: "400rpx",
maxLines: 1,
},
common,
],
},
{
type: "text",
text: "textStyle: 'stroke'",
css: [
{
top: `${startTop + 8 * gapSize}rpx`,
textStyle: "stroke",
fontWeight: "bold",
},
common,
],
},
],
};
}
}
const startTop = 50;
const gapSize = 70;
const common = {
left: "20rpx",
fontSize: "40rpx",
};
function _textDecoration(decoration, index, color) {
return {
type: "text",
text: decoration,
css: [
{
top: `${startTop + index * gapSize}rpx`,
color: color,
textDecoration: decoration,
},
common,
],
};
}
$ claude mcp add Painter \
-- python -m otcore.mcp_server <graph>