---
title: WKPosterPainter 海报
group:
  title: 高级
  path: /advanced/
---

## 海报

<br>

此组件是在[Painter](https://github.com/Kujiale-Mobile/Painter) 基础之上进行裁剪和优化。

想到小程序中有如此大量的生成海报需求，而 Canvas 生成方法又是如此难用和坑爹、后期难以维护。我们就想到可不可以做一款可以很方便生成图片，并且还能屏蔽掉直接使用 Canvas 的一些坑的库呢？

同构 WKPosterPainter 组件，你只需要声明式地定义需要绘制的 JSON DSL 即可，用法和 HTML/CSS 类似。

<br>
<br>

**WKPosterPainter 的特性**

- 功能全，支持文本、图片、矩形、qrcode 类型的 view 绘制
- 布局全，支持多种布局方式，如 align（对齐方式）、rotate（旋转）
- 支持圆角，其中图片，矩形，和整个画布支持 borderRadius 来设置圆角
- 支持边框，同时支持 solid、dashed、dotted 三种类型
- 支持渐变色，包括线性渐变与径向渐变。
- 支持 box-shadow 和 text-shadow，统一使用 shadow 表示。
- 支持文字背景、获取宽度、主动换行
- 支持自定义字体
- 支持图片 mode
- 支持元素的相对定位方法
- 杠杠的性能优化，我们对网络素材图片加载实现了一套 LRU 存储机制，不用重复下载素材图片。
- 杠杠的容错，因为某些特殊情况会导致 Canvas 绘图不完整。我们对此加入了对结果图片进行检测机制，如果绘图出错会进行重绘。
- 生成的图片支持分辨率调节
- 支持使用拖动等操作动态编辑绘制内容

<br>
<br>

## Palette 规范

首先你需要声明一个 Palette 调色板，他的用法和你熟悉的 HTML/CSS 非常类似, 一个 Palatte 包含以下属性：

```tsx | pure
export interface IPalette {
  background?: string; // 整个模版的背景，支持网络图片的链接、纯色和渐变色
  width: string;
  height: string;
  borderRadius?: string;
  views: IView[];
}
```

- **background**: 可以是颜色值，也可以为网络图片的链接，默认为白色，支持渐变色
- **width**: 宽度
- **height**: 高度
- **borderRadius**: 边框的圆角（该属性也同样适用于子 view）
- **views**: 里面承载子 view

<br>
<br>

### View 规范

当我们把整体的调色板属性构建起来后，里面就可以添加子 View 来进行绘制了。

```tsx | pure
export interface IView {
  // 视图类型
  type?: 'rect' | 'text' | 'image' | 'qrcode';
  text?: string;
  url?: string;
  id?: string;
  /** 事实上painter中view的css属性并不完全与CSSProperties一致。 */
  /** 有一些属性painter并不支持，而当你需要开启一些“高级”能力时，属性的使用方式也与css规范不一致。 */
  /** 具体的区别我们将在下方对应的view介绍中详细讲解，在这里使用CSSProperties仅仅是为了让你享受代码提示 */
  css: CSSProperties | any;
  content?: string;
  rect?: {
    left: number;
    top: number;
    right: number;
    bottom: number;
    x: number;
    y: number;
    minWidth?: number;
  };
}
```

<br>
<br>

View 的几种类型：

| 类型   | 内容    | 描述                           | 自有 css                                                          |
| ------ | ------- | ------------------------------ | ----------------------------------------------------------------- |
| image  | url     | 表示图片资源的地址，本地或网络 | 见 image 小节                                                     |
| text   | text    | 文本的内容                     | 见 text 小节                                                      |
| rect   | 无      | 矩形                           | color: 颜色，支持渐变色                                           |
| qrcode | content | 画二维码                       | background: 背景颜色（默认为透明色）color: 二维码颜色（默认黑色） |

<br>
<br>

<br>
<br>

#### image

Painter 的 image 可以设置成本地图片或者网络图片，注意本地图片请使用绝对路径。并且如果未设置 image 的长宽，则长宽的属性值会默认设为 auto。若长宽均为 auto 则会使用图片本身的长宽来布局，大小为图片的像素值除以
pixelRatio 。

| 属性名称 | 说明                 | 默认值     |
| -------- | -------------------- | ---------- |
| width    | image 的宽度         | auto       |
| height   | image 的高度         | auto       |
| mode     | 图片裁剪、缩放的模式 | aspectFill |

**scaleToFill**：不保持纵横比缩放图片，使图片的宽高完全拉伸至填满 image 元素

**aspectFill**：保持纵横比缩放图片，只保证图片的短边能完全显示出来。也就是说，图片通常只在水平或垂直方向是完整的，另一个方向将会发生截取。

**注：mode 属性和小程序 image 的 mode 属性功能一致，只是支持的类型只有两种，且默认值不同。 当 width 或 height 属性设置为 auto 时，mode 属性失效**

<br>
<br>

**示例代码**

![测试结果](./exm.png)

```tsx
import React, { useState, useMemo } from 'react';
import PageWrapper from '../PageWrapper';
import { View, WKPosterPainter, WKPage, IPalette, WKImage } from '@wakeapp/components';
import Example from './example';

const PosterPainterPage = () => {
  const palette = useMemo<IPalette>(() => {
    return {
      width: '654rpx',
      height: '1000rpx',
      background: '#eee',
      views: [
        {
          type: 'image',
          url: 'https://img.iplaysoft.com/wp-content/uploads/2019/free-images/free_stock_photo.jpg',
        },
        {
          type: 'text',
          text: '未设置height、width时',
          css: {
            right: '0rpx',
            top: '60rpx',
            fontSize: '30rpx',
          },
        },
        {
          type: 'image',
          url: 'https://img.iplaysoft.com/wp-content/uploads/2019/free-images/free_stock_photo.jpg',
          css: {
            width: '200rpx',
            height: '200rpx',
            top: '230rpx',
          },
        },
        {
          type: 'text',
          text: "mode: 'aspectFill' 或 无",
          css: {
            left: '210rpx',
            fontSize: '30rpx',
            top: '290rpx',
          },
        },
        {
          type: 'image',
          url: 'https://img.iplaysoft.com/wp-content/uploads/2019/free-images/free_stock_photo.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: 'https://img.iplaysoft.com/wp-content/uploads/2019/free-images/free_stock_photo.jpg',
          css: {
            width: '200rpx',
            height: 'auto',
            top: '750rpx',
          },
        },
        {
          type: 'text',
          text: '设置height为auto',
          css: {
            left: '210rpx',
            top: '780rpx',
            fontSize: '30rpx',
          },
        },
      ],
    };
  }, []);

  return (
    <PageWrapper>
      <WKPosterPainter
        palette={palette}
        width="100%"
        onImgOK={img => {
          // 生成的图片地址，可以用于分享
          console.log(img);
        }}
        onImgErr={err => {
          console.log('poster err', err);
        }}
      />
    </PageWrapper>
  );
};

export default PosterPainterPage;
```

<br>
<br>
<br>

#### text

因为 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 属性，则最大展示所设置的行数，如果还有多余内容未展示出来，则后面会带上 ... 。

<br>

##### 自定义字体

从 2.13.0 版本基础库开始，我们可以： 在 app.js onLaunch
的时候添加你的[自定义字体](https://developers.weixin.qq.com/miniprogram/dev/api/ui/font/wx.loadFontFace.html)。但是有三点要注意:

1. painter 组件一定要用使用 use2d。
2. 一定要使用 scopes，`scopes: ['webview', 'native']`
3. 在开发工具上是看不到效果的，要在手机上测试。

```js | pure
wx.loadFontFace({
  global: true,
  family: 'xxxx',
  source: 'https://xxxx.ttf',
  scopes: ['webview', 'native'],
});
```

<br>
<br>

**以下用个例子说下上述几个属性的用法**

![测试结果](./exm1.png)

**示例代码**

```tsx
import React, { useState, useMemo } from 'react';
import PageWrapper from '../PageWrapper';
import { View, WKPosterPainter, WKPage, IPalette, WKImage } from '@wakeapp/components';

const startTop = 50;
const gapSize = 70;
const common = {
  left: '20rpx',
  fontSize: '40rpx',
};

function _textDecoration(decoration: string, index: number, color?: string) {
  return {
    type: 'text',
    text: decoration,
    css: [
      {
        top: `${startTop + index * gapSize}rpx`,
        color: color,
        textDecoration: decoration,
      },
      common,
    ],
  };
}

const PosterPainterPage = () => {
  const palette = useMemo<IPalette>(() => {
    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,
          ],
        },
      ],
    };
  }, []);

  return (
    <PageWrapper>
      <WKPosterPainter
        palette={palette}
        width="100%"
        onImgOK={img => {
          console.log(img);
        }}
        onImgErr={err => {
          console.log('poster err', err);
        }}
      />
    </PageWrapper>
  );
};

export default PosterPainterPage;
```

### 布局属性

以上 View ，除去自己拥有的特别属性外，还有以下的通用布局属性

| 属性                     | 说明                                           | 默认                  |
| ------------------------ | ---------------------------------------------- | --------------------- |
| rotate                   | 旋转，按照顺时针旋转的度数                     | 不旋转                |
| width、height            | view 的宽度和高度，其中 image 和 text 可不设置 |                       |
| top、right、bottom、left | 如 css 中为 absolute 布局时的作用，可为 负值   | 默认 top 和 left 为 0 |

![](https://user-images.githubusercontent.com/4279515/46778627-290ead80-cd47-11e8-8483-2e36e39b36f0.png)

<br>
<br>

#### 相对布局

1，在需要暴露自己位置的信息的元素上增加一个 id 属性，注意此 id 中不要包含加减乘除符号，如下：

```
{
  id: 'myTextId',
  type: 'text',
  ...
}
```

2，然后在后面的 view 中，你可以在 left、right、top、bottom、width、height 属性中使用 calc 属性。如下

```
left: 'calc(myTextId.bottom + 100rpx)'
width: 'calc(myTextId.width * 2)'
```

![](https://qhstaticssl.kujiale.com/newt/165/image/png/1624271816278/AF8DE4AEA90102C2220247E0244EF2C0.png)

<br>
<br>

**例子代码**

```tsx
import React, { useState, useMemo } from 'react';
import PageWrapper from '../PageWrapper';
import { View, WKPosterPainter, WKPage, IPalette, WKImage } from '@wakeapp/components';
import Example from './example1';

const PosterPainterPage = () => {
  const palette = useMemo<IPalette>(() => {
    return new Example().palette() as IPalette;
  }, []);

  return (
    <PageWrapper>
      <View style={{ display: 'flex', justifyContent: 'center' }}>
        <WKPosterPainter
          palette={palette}
          width="100%"
          onImgOK={imgs => {
            console.log(imgs);
          }}
          onImgErr={err => {
            console.log('poster err', err);
          }}
        />
      </View>
    </PageWrapper>
  );
};

export default PosterPainterPage;
```

**注：相对布局的 view 代码一定需要在被相对的 view 的之后。**

#### border 类型

| 属性         | 说明                                                           | 默认                   |
| ------------ | -------------------------------------------------------------- | ---------------------- |
| borderRadius | 边界圆角程度，如果是正方形布局，该属性为一半宽或高时，则为圆形 | 0                      |
| borderWidth  | 边界宽度，外边界                                               | 必设值，否则无边框效果 |
| borderColor  | 边框颜色                                                       | black                  |
| borderStyle  | 边框样式，支持 dashed、dotted、solid                           | solid                  |

![](https://user-images.githubusercontent.com/4279515/46778646-3cba1400-cd47-11e8-916a-3fddc172534d.png)

<br>
<br>

#### align

Painter 的 align 类型与 css 中的 align 有些许不同。在 Painter 中 align 表示 view 本身的对齐方式，而不像 css 中表示对其子 view 的操作。align 可以作用在 Painter
支持的所有 view 上。它以设置的 left、top、right、bottom 的位置为基准，然后做不同的对齐操作。并且 align 在文字多行情况下，会影响多行文字的对齐方式。

**注意：如果布局使用了 right 确定位置，则该 view 会默认右对齐布局，但此时文字还是从左边绘制。**

![](https://user-images.githubusercontent.com/4279515/46778660-4e9bb700-cd47-11e8-8d93-e522185e8188.png)

<br>
<br>

**示例代码**

```tsx
import React, { useState, useMemo } from 'react';
import PageWrapper from '../PageWrapper';
import { View, WKPosterPainter, WKPage, IPalette, WKImage } from '@wakeapp/components';
import Example from './example2';

const PosterPainterPage = () => {
  const palette = useMemo<IPalette>(() => {
    return new Example().palette() as IPalette;
  }, []);

  return (
    <PageWrapper>
      <View style={{ display: 'flex', justifyContent: 'center' }}>
        <WKPosterPainter
          palette={palette}
          width="100%"
          onImgOK={imgs => {
            console.log(imgs);
          }}
          onImgErr={err => {
            console.log('poster err', err);
          }}
        />
      </View>
    </PageWrapper>
  );
};

export default PosterPainterPage;
```

<br>
<br>

### CSS3 支持

#### shadow

Painter 中的 shadow 可以同时修饰 image、rect、text、qrcode 等 。在修饰 text 时则相当于 text-shadow；修饰 image 和 rect 时相当于 box-shadow；修饰 qrcode
时，则相当于二维码有效区域的投影。

![](https://user-images.githubusercontent.com/4279515/51457535-ab6a2d00-1d8c-11e9-8812-9ab1ee8dafa4.png)

使用方法：

```
shadow: 'h-shadow v-shadow blur color';
h-shadow: 必需。水平阴影的位置。允许负值。
v-shadow: 必需。垂直阴影的位置。允许负值。
blur: 必需。模糊的距离。
color: 必需。阴影的颜色。
```

<br>
<br>

**示例代码**

```tsx
import React, { useState, useMemo } from 'react';
import PageWrapper from '../PageWrapper';
import { View, WKPosterPainter, WKPage, IPalette, WKImage } from '@wakeapp/components';
import Example from './shadowExample';

const PosterPainterPage = () => {
  const palette = useMemo<IPalette>(() => {
    return new Example().palette() as IPalette;
  }, []);

  return (
    <PageWrapper>
      <View style={{ display: 'flex', justifyContent: 'center' }}>
        <WKPosterPainter
          palette={palette}
          width="100%"
          onImgOK={imgs => {
            console.log(imgs);
          }}
          onImgErr={err => {
            console.log('poster err', err);
          }}
        />
      </View>
    </PageWrapper>
  );
};

export default PosterPainterPage;
```

<br>
<br>

#### 渐变色支持

你可以在画布的 background 属性或者 rect 的 color 属性中使用以下方式实现 css 3 的渐变色，其中 radial-gradient 渐变的圆心为 view 中点，半径为最长边，目前不支持自己设置。

```css
 {
  background: linear-gradient(-135deg, blue 0%, rgba(18, 52, 86, 1) 20%, #987 80%);

  color: radial-gradient(rgba(0, 0, 0, 0) 5%, #0ff 15%, #f0f 60%);
}
```

**！！！注意：颜色后面的百分比一定得写。**

<br>
<br>

### 其他技巧

#### 文字竖行显示

因为 Painter 支持换行符，所以我们可以配合向字符之间插入换行符来达到竖排显示的效果，并且我们还能自由控制是从左到右或从右到左，如下图所示。

![竖排效果](https://user-images.githubusercontent.com/4279515/61357471-f16efc00-a8aa-11e9-84b3-192fe158f38d.png)

<br>

**示例代码**

```tsx
import React, { useState, useMemo } from 'react';
import PageWrapper from '../PageWrapper';
import { View, WKPosterPainter, WKPage, IPalette, WKImage } from '@wakeapp/components';
import Example from './example3';

const PosterPainterPage = () => {
  const palette = useMemo<IPalette>(() => {
    return new Example().palette() as IPalette;
  }, []);

  return (
    <PageWrapper>
      <View style={{ display: 'flex', justifyContent: 'center' }}>
        <WKPosterPainter
          width="100%"
          palette={palette}
          onImgOK={imgs => {
            console.log(imgs);
          }}
          onImgErr={err => {
            console.log('poster err', err);
          }}
        />
      </View>
    </PageWrapper>
  );
};

export default PosterPainterPage;
```

<br>
<br>

## Tips（一定要看哦～）

1，目前 Painter 中支持两种尺寸单位，px 和 rpx，代表的意思和小程序中一致，此处就不多说。

2，目前子 view 的 css 属性支持 object 或 array。所以意味着，你可以把几个子 view 共用的 css 属性提取出来。做到让 Palette 更加简洁。

3，因为我们的 palette 是以 js 承载的 json，所以意味着你可以在每一个属性中很方便的加上自己的逻辑。也可以把某些属性单独提取出来，让多个 palette 共用，做到模块化。

<br>
<br>

## 组件文档

<API src='@wakeapp/components/WKPosterPainter/index.d.ts'></API>

<br>
<br>
