Skip to content

基于 Taro3 的微信小程序 canvas 绘图组件,封装了常用的操作,通过配置的方式绘制海报

Notifications You must be signed in to change notification settings

jianhao/taro-canvas

Repository files navigation

taro-canvas

小程序组件-小程序海报组件 npmnpm

概述

taro-canvas 是基于 Taro3 的微信小程序 canvas 绘图组件,封装了常用的操作, 通过配置的方式绘制海报。

生成效果

安装使用

方式一:通过 npm 安装 (推荐)

小程序已经支持使用 npm 安装第三方包,详见 npm 支持

# npm
npm i taro-canvas -S --production

# yarn
yarn add taro-canvas --production

使用组件

// 引入代码
import { TaroCanvas } from 'taro-canvas';

// 在 render 方法中调用
<TaroCanvas
  config={this.state.config}
  onCreateSuccess={this.onCreateSuccess}
  onCreateFail={this.onCreateFail}
/>
// 注意点 
// config 绘图配置信息 - 必填项
// onCreateSuccess 绘图成功回调 - 必须实现 => 接收绘制结果、重置 TaroCanvas 状态
// onCreateFail 绘图失败回调 - 必须实现 => 接收绘制错误信息、重置 TaroCanvas 状态

方式二: 下载代码

直接通过 git 下载 taro-canvas 源代码,并将src/component/TaroCanvas目录拷贝到自己的项目的 src/component目录中

使用组件

// 引入代码 *引入方式和上面的方式一略有不同
import TaroCanvas from '../../component/TaroCanvas';

// 在 render 方法中调用 和方式一一样

使用注意事项

  1. 图片的域名务必添加到 downloadFile 合法域名中(开发设置-服务器域名-downloadFile合法域名) 【P.s 开发时可 选中不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书】 【P.s 真机运行,可打开调试模式】

组件参数解释

config字段

字段 类型 必填 描述
width Number(单位:rpx) 画布宽度
height Number(单位:rpx) 画布高度
backgroundColor String 画布颜色
debug Boolean false隐藏canvas,true显示canvas,默认false
blocks Object Array(对象数组) 看下文
texts Object Array(对象数组) 看下文
images Object Array(对象数组) 看下文
lines Object Array(对象数组) 看下文

blocks字段

字段名 类型 必填 描述
x Number(单位:rpx) 块的坐标
y Number(单位:rpx) 块的坐标
width Number(单位:rpx) 如果内部有文字,由文字宽度和内边距决定
height Number(单位:rpx)
paddingLeft Number(单位:rpx) 内左边距
paddingRight Number(单位:rpx) 内右边距
borderWidth Number(单位:rpx) 边框宽度
borderColor String 边框颜色
backgroundColor String 背景颜色
borderRadius Number(单位:rpx) 圆角
text Object 块里面可以填充文字,参考texts字段解释
zIndex Int 层级,越大越高

texts字段

字段名 类型 必填 描述
x Number(单位:rpx) 坐标
y Number(单位:rpx) 坐标
text String|Object 当Object类型时,参数为text字段的参数,marginLeft、marginRight这两个字段可用(示例请看下文)
fontSize Number(单位:rpx) 文字大小
color String 颜色
opacity Int 1为不透明,0为透明
lineHeight Number(单位:rpx) 行高
lineNum Int 根据宽度换行,最多的行数
width Number(单位:rpx) 没有指定为画布宽度
marginLeft Number(单位:rpx) 当text字段为Object可以使用,用来控制多行文字间距
marginRight Number(单位:rpx) 当text字段为Object可以使用,用来控制多行文字间距
textDecoration String 目前只支持 line-through(贯穿线),默认为none
baseLine String top| middle|bottom基线对齐方式
textAlign String left|center|right对齐方式
zIndex Int 层级,越大越高
fontFamily String 小程序默认字体为'sans-serif', 请输入小程序支持的字体,例如:'STSong'
fontWeight String 'bold'加粗字体,目前小程序不支持 100 - 900 加粗
fontStyle String 'italic'倾斜字体

images字段

字段 类型 必填 描述
x Number(单位:rpx) 右上角的坐标
y Number(单位:rpx) 右上角的坐标
url String 图片url(需要添加到下载白名单域名中)也支持本地图片
width Number(单位:rpx) 宽度(会根据图片的尺寸同比例缩放
height Number(单位:rpx) 高度(会根据图片的尺寸同比例缩放
borderRadius Number(单位:rpx) 圆角,跟css一样
borderWidth Number(单位:rpx) 边框宽度
borderColor String 边框颜色
zIndex Int 层级,越大越高

lines字段

字段 类型 必填 描述
startX Number(单位:rpx) 起始坐标
startY Number(单位:rpx) 起始坐标
endX Number(单位:rpx) 终结坐标
endY Number(单位:rpx) 终结坐标
width Number(单位:rpx) 线的宽度
color String 线的颜色
zIndex Int 层级,越大越高

事件

onCreateSuccess(绘制成功回调)

返回生成海报图片的本地 url,一般做法是使用 wx.previewImage 预览海报或者在指定位置预览,如下

// 绘制成功回调函数 (必须实现)=> 接收绘制结果、重置 TaroCanvas 状态
  onCreateSuccess = (result) => {
    const { tempFilePath, errMsg } = result;
    Taro.hideLoading();
    if (errMsg === 'canvasToTempFilePath:ok') {
      this.setState({
        shareImage: tempFilePath,
        // 重置 TaroCanvas 状态
        canvasStatus: false,
        config: null
      })
    } else {
      // 重置 TaroCanvas 状态
      this.setState({
        canvasStatus: false,
        config: null
      })
      Taro.showToast({ icon: 'none', title: errMsg || '出现错误' });
      console.log(errMsg);
    }
  }

onCreateFail(绘制失败)

// 绘制失败回调函数 (必须实现)=> 接收绘制错误信息、重置 TaroCanvas 状态
  onCreateFail = (error) => {
    Taro.hideLoading();
    // 重置 TaroCanvas 状态
    this.setState({
      canvasStatus: false,
      config: null
    })
    console.log(error);
  }

参数注意事项

text

  1. 每个元素的 zIndex 属性决定在图层中的高度,其中 text 属性为数组时,继承父元素的 zIndex
  2. text 为数组时,多段文字的位置基于父元素坐标和自身 margin 进行计算
  3. text 为数组时,多段文字超出宽度暂无处理,不建议绘制多段文字超出

Block

  1. 支持圆角、边框、背景色的矩形
  2. 块内文字暂不支持多段,多段文字请使用 texts 定位到该块内

Demo

以下是示例海报的详细代码,也可以clone仓库,去 src/pages/demo 具体实现。

例子代码(点击展开)
import { useState, useCallback, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { View, Image, Button, Canvas } from '@tarojs/components'
import classnames from 'classnames'
import cloneDeep from 'lodash/clonedeep'
import selectedImg from '@/assets/selected.png'
import { saveImgAlbum, queryAuth } from '@/utils/nativeApi'
import TaroCanvas from '@/components/TaroCanvas'
import { DrawConfig } from '@/components/TaroCanvas/types'
import styles from './index.module.less'

// 默认海报配置
const defaultConfig = {
  width: 750,
  height: 1334,
  backgroundColor: '#eee',
  debug: false,
  blocks: [
    // 底部白色
    {
      x: 30,
      y: 30,
      width: 690,
      height: 1274,
      paddingLeft: 0,
      paddingRight: 0,
      backgroundColor: '#fff',
      zIndex: 10,
    },
    { // �公司框框
      x: 60,
      y: 855,
      width: 630,
      height: 212,
      paddingLeft: 0,
      paddingRight: 0,
      borderWidth: 2,
      opacity: 0.3,
      borderColor: 'green',
      backgroundColor: '#fff',
      zIndex: 30,
      borderRadius: 10,
    },
    // 头图渐变遮罩
    {
      x: 30,
      y: 432,
      width: 690,
      height: 98,
      paddingLeft: 0,
      paddingRight: 0,
      backgroundColor: 'linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #fff 100%)',
      zIndex: 30,
    },
    // 外渐变边框
    {
      x: 30,
      y: 30,
      width: 690,
      height: 107,
      paddingLeft: 0,
      paddingRight: 0,
      textShadow: '0px 2px 0px rgba(0, 0, 0, 0.5)',
      backgroundColor: 'background: linear-gradient(180deg, rgba(0, 0, 0, 0.66) 0%, rgba(0, 0, 0, 0) 100%)',
      zIndex: 30,
    },
    // 个人二维码边框
    {
      x: 157,
      y: 1124,
      width: 126,
      height: 126,
      paddingLeft: 0,
      paddingRight: 0,
      borderWidth: 2,
      opacity: 1,
      borderColor: '#4069F6',
      backgroundColor: '#fff',
      zIndex: 30,
    },
  ],
  texts: [
    {
      x: 97,
      y: 62,
      text: '',
      fontSize: 24,
      color: '#fff',
      lineHeight: 33,
      lineNum: 1,
      width: 500,
      zIndex: 50,
    },
    {
      x: 65,
      y: 547,
      text: '剑豪向你推荐好车',
      color: '#1E63ED',
      fontSize: 32,
      lineHeight: 45,
      zIndex: 50,
    },
    {
      x: 65,
      y: 615,
      text: '玛莎拉蒂 2019款 40 TFSI 时尚版',
      width: 630,
      lineNum: 2, // 最多几行
      fontSize: 48,
      fontWeight: 'bold',
      color: 'rgba(0, 0, 0, 0.9)',
      lineHeight: 67,
      zIndex: 50,
    },
    {
      x: 65,
      y: 758,
      zIndex: 100,
      text: [
        {
          text: '275.23',
          fontSize: 54,
          fontWeight: 'bold',
          color: 'red',
          lineNum: 1,
          zIndex: 1000,
        },
        {
          marginLeft: 10,
          marginRight: 30,
          marginTop: 20,
          text: '万元',
          width: 200,
          fontSize: 28,
          fontWeight: 'bold',
          color: '#FF2322',
          lineNum: 1,
          zIndex: 50,
        },
        {
          marginTop: 20,
          text: '指导价 8.98 万',
          fontSize: 28,
          color: 'rgba(0, 0, 0, 0.6)',
          lineNum: 1,
          zIndex: 50,
        },
      ],
    },
    {
      x: 95,
      y: 889,
      marginLeft: 35,
      text: '珍珠白/棕色/米黄色 中规车',
      fontSize: 26,
      width: 386,
      color: 'rgba(0, 0, 0, 0.6)',
      zIndex: 50,
    },
    {
      x: 95,
      y: 943,
      text: '我是一段没有感情的备注说明备注说明备注说明备注说明备注说明备注说明备注说明备注说明',
      lineNum: 3,
      // width: 386,
      width: 547,
      lineHeight: 37,
      fontSize: 26,
      color: 'rgba(0, 0, 0, 0.6)',
      zIndex: 50,
    },
    {
      x: 348,
      y: 1131,
      text: '购买请联系 剑豪',
      fontSize: 24,
      color: '#1E63ED',
      lineHeight: 33,
      lineNum: 1,
      zIndex: 50,
    },
    {
      x: 348,
      y: 1164,
      text: '扫描左侧二维码添加微信',
      fontSize: 24,
      color: '#1E63ED',
      lineHeight: 33,
      lineNum: 1,
      zIndex: 50,
    },
    {
      x: 348,
      y: 1207,
      text: '13566661111',
      fontSize: 36,
      fontWeight: 'bold',
      color: '#4069F6',
      lineHeight: 49,
      lineNum: 1,
      zIndex: 50,
    },
  ],
  images: [
    { // 车辆头图
      x: 30,
      y: 30,
      width: 690,
      height: 500,
      url: 'http://img.jianhunxia.com/imgs/ChMkJ1gi9H-IcjFjAAYz8EtaA9IAAXm5gOMcEUABjQI228.jpg',
      zIndex: 20,
    },
    // 个人二维码
    {
      x: 163,
      y: 1130,
      width: 114,
      height: 114,
      url: 'http://pic.jianhunxia.com/imgs/20210909154455.jpeg',
      zIndex: 50,
    },
    // 圆角头像
    {
      x: 538,
      y: 1128,
      width: 26,
      height: 26,
      borderColor: "red",
      borderWidth: 1,
      borderRadius: 13,
      url: 'https://pic.jianhunxia.com/blog/jianhao-avatar.jpg',
      zIndex: 50,
    },
  ],
}

const COLOR_TYPES = [
  { id: 1, type: 'Blue', color: 'linear-gradient(116deg, #8454FF 0%, #005FFF 100%)' },
  { id: 2, type: 'Green', color: 'linear-gradient(180deg, rgba(180, 236, 81, 1) 0%, rgba(66, 147, 33, 1) 100%)' },
  { id: 3, type: 'Purple', color: 'linear-gradient(135deg, rgba(48, 35, 174, 1) 0%, rgba(200, 109, 215, 1) 100%)' },
  { id: 4, type: 'Red', color: 'linear-gradient(0deg, #F7A21C 0%, #FF023F 100%)' },
]

function Index () {
  const [crawLoading, setCrawLoading] = useState(false) // 绘制中
  const [canvasStatus, setCanvasStatus] = useState(false) // 展示 canvas 组件
  const [config, setConfig] = useState<DrawConfig>(defaultConfig) // 海报配置参数
  const [currentColor, setCurrentColor] = useState(COLOR_TYPES[0]) // 背景色选项
  const [tempImgs, setTempImgs] = useState<any>([]) // 海报配置参数

  // 改变背景
  const changeBg = async item => {
    setCurrentColor(item)
  }

  // 测量文本宽度
  const getTextWidth = textData => new Promise(resolve => {
    setTimeout(() => { // 不延时可能会
      const { text, fontWeight, fontSize, fontFamily } = textData || {}
      const pageInstance = Taro.getCurrentInstance()?.page || {} // 拿到当前页面实例
      const query = Taro.createSelectorQuery().in(pageInstance)
      query.select('#tempCanvas').fields({ node: true, size: true, context: true }, res => {
        const canvas = res.node
        const ctx = canvas.getContext('2d')
        ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`
        const { width } = ctx.measureText(text)
        resolve(width)
      }).exec()
    }, 300);
  })

  useEffect(() => {
    beginDraw()
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  // 调用绘画 显示 canvas 组件、同时设置 config
  const beginDraw = async () => {
    if (crawLoading) return
    setCrawLoading(true)
    setTempImgs([]) // 先清空一波图片缓存
    const newConfig = cloneDeep(defaultConfig)
    newConfig.backgroundColor = currentColor.color

    // 注意:必须要先设置 config,再去让组件显示
    setConfig(newConfig)
    setCanvasStatus(true)
    Taro.showLoading({
      title: '绘制中...',
    })
  }

  // 绘制后保存图片
  const saveImg = useCallback(async () => {
    setCrawLoading(false)
    return // 需要保存到本地就去除这行代码
    const hasAuth = await queryAuth('scope.writePhotosAlbum')
    console.log('有没有权限保存图片', hasAuth);
    if (hasAuth) {
      console.log('缓存图片', tempImgs)
      const taskList = tempImgs.map((url, index) => saveImgAlbum(url, index + 1)) // 添加保存图片的任务
      await Promise.all(taskList)
      Taro.showToast({ icon: 'none', title: '海报已保存到本地' })
    } else { // 如果拒绝授权相册, 引导用户去授权
      Taro.showToast({ icon: 'none', title: '请授权小程序获取相册权限' })
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [tempImgs])

  useEffect(() => {
    if (tempImgs?.length > 0) {
      saveImg()
    }
  }, [saveImg, tempImgs])

  // 绘制失败
  const onCreateFail = err => {
    Taro.hideLoading()
    setCrawLoading(false)
    Taro.showToast({ icon: 'none', title: err || '绘制出现错误' })
  }

  // 海报绘制成功
  const onCreateSuccess = (result) => {
    console.log('绘制好了', result);
    const { tempFilePath, errMsg } = result
    let msg = '绘制失败'
    setCanvasStatus(false) // 想查看调试就先不要隐藏
    msg = '海报绘制出现错误'
    if (errMsg === 'canvasToTempFilePath:ok') {
      setTempImgs([...tempImgs, tempFilePath])
    } else {
      onCreateFail(msg)
    }
  }

  return (
    <View className={styles.posterPreview}>
      <Image
        src={tempImgs[0]}
        className={styles.posterImg}
        mode='widthFix'
      />
      {/* 底部按钮 */}
      <View className={styles.btnBox}>
        <View className={styles.btnWrap}>
          {
            COLOR_TYPES.map(item => (
              <View
                key={item.id}
                className={classnames(styles.bgItem, styles[`item${item.type}`])}
                onClick={() => changeBg(item)}
              >
                {(item.id === currentColor.id) && (
                  <Image src={selectedImg} className={styles.selected} />
                )}
              </View>
            ))
          }
        </View>
        <Button className={styles.saveBtn} onClick={beginDraw}>绘制海报</Button>
      </View>

      { // 由于小程序限制,目前组件通过状态的方式来动态加载
        canvasStatus && (
          <TaroCanvas
            config={config} // 绘制配置
            onCreateSuccess={onCreateSuccess} // 绘制成功回调
            onCreateFail={onCreateFail} // 绘制失败回调
          />
        )
      }

      {/* 用于计算文字宽度的canvas */}
      <Canvas
        type='2d'
        id='tempCanvas'
        className={styles.tempCanvas}
      />
    </View>
  )
}

Index.config = {
  navigationBarTitleText: '绘制海报',
}

export default Index

样式文件

.posterPreview {
  width: 100%;
  height: 100vh;
  padding-bottom: constant(safe-area-inset-bottom); /* 兼容 IOS<11.2 */
  padding-bottom: env(safe-area-inset-bottom); /* 兼容 IOS>11.2 */
  background: rgba(248, 248, 248, 1);
  padding-bottom: 350px;

  .posterImg{
    width: 690px;
    margin: 50px 30px;
  }

  // 底部按钮
  .btnBox {
    position: fixed;
    bottom: 0;
    left: 0;
    z-index: 500;
    box-sizing: border-box;
    width: 100%;
    padding: 27px 30px;
    padding-bottom: calc(27px + constant(safe-area-inset-bottom)); /* 兼容 IOS<11.2 */
    padding-bottom: calc(27px + env(safe-area-inset-bottom)); /* 兼容 IOS>11.2 */
    background: #fff;

    .saveBtn {
      line-height: 90px;
      width: 690px;
      height:90px;
      font-size: 30px;
      color: #fff;
      background: linear-gradient(213deg, #5495FF 0%, #2F5CF5 100%);
    }

    .btnWrap {
      display: flex;
      justify-content: space-around;
      width: 690px;
      margin: 0 auto;
      margin-bottom: 20px;

      .bgItem {
        position: relative;
        width: 66px;
        height: 66px;
        border-radius: 33px;

        .selected {
          position: absolute;
          top: -5px;
          right: -5px;
          width: 30px;
          height: 30px;
          border-radius: 15px;
        }
      }
    }
  }

  .itemBlue {
    background: linear-gradient(136deg, #8454ff 0%, #005fff 100%);
  }

  .itemGreen {
    background: linear-gradient(180deg, rgba(180, 236, 81, 1) 0%, rgba(66, 147, 33, 1) 100%);
  }

  .itemPurple {
    background: linear-gradient(135deg, rgba(48, 35, 174, 1) 0%, rgba(200, 109, 215, 1) 100%);
  }

  .itemRed {
    background: linear-gradient(0deg, #f7a21c 0%, #ff023f 100%);
  }

  .tempCanvas {
    position: absolute;
    top: -10000px;
    left: 0;
    width: 750px;
    height: 1000px;
  }
}

问题反馈

有什么问题可以直接提issue

提issue

参考

About

基于 Taro3 的微信小程序 canvas 绘图组件,封装了常用的操作,通过配置的方式绘制海报

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published