taro canvas 生成海报

前言

  • 📝 一下这几天写小程序,使用 canvas 动态生成海报,下载分享的效果
  • 使用 taro 做小程序,因为可以用 React,使用了最新的 React hooks 的方式(好像也不新了)
  • 刚开始写感觉很累,因为没有搞过小程序,并且也不会用 canvas,我只能写一点,编译一下看效果👀,走了很多弯路,花了很多心思,结果出来后,感觉自己太笨了(学习能力还有待提高)


    成品效果.png
  • 按钮样式还没有改,等成品出来改一下 (打个小广告)

下载网络图片,处理多张图片

  • 因为要动态画头像,需要把网络图片地址,下载后画到 canvas 中

封装一个处理多张图片的 promise

  • 因为不想使用 getImageInfo 一层层嵌套,图片很多的话,岂不是乱的像🐶一样。
// 处理多张网络图片
  const processMultipleImages = (url) => {
    return new Promise((resolve, reject) => {
      Taro.getImageInfo({
        src: url,
        success: (res) => {
          resolve(res)
        },
        fail: () => {
          Taro.showToast({
            title: '下载失败!'
          })
        }
      })
    })
  }

处理所有图片

  • 使用 promise.all 转换所有的图片地址
 // 获取基本信息
  useEffect(() => {
    // 获取所有的网络图片
    Promise.all(
      imageAry.map(img => processMultipleImages(img))
    ).then(images => {
      const imgAll = images.map(i => i.path)
      SetUrl(imgAll)
    })
  }, [])

画主体

获取屏幕信息

  • 主要是获取宽度和高度,动态给 canvas 设置宽高,已经 canvas 间距,rate 表示转换的倍数(根据设计稿的标准宽高),目前是用的这种方式,网上看了很多都没有转换,难道不用根据设备适配吗?还是说我用的方式不对,业务催得紧没有仔细研究,如果你知道的话,欢迎交流~
    const d = Taro.getSystemInfoSync()
    const w = d.windowWidth * 0.85
    const h = (w / 0.75).toFixed(2)
    const rate = (d.windowWidth / 375).toFixed(2)
    SetWidth(w)
    SetHeight(h)
    SetRate(rate)
  • 宽度和高度
  return (
    <View className="share-user-container">
      <Canvas style={{width: `${width}px`, height: `${height}px`}} canvasId="shareuser" id="shareuser" className="canvas-wrapper"></Canvas>
      <Button onClick={onClickSaveImage}>保存到相册</Button>
    </View>
  )

圆形头像

    const ctx = Taro.createCanvasContext('shareuser');
   // 画内圆 并 填充头像
    ctx.beginPath()
    const x = 56 * rate
    const y = 74 * rate + 64 * rate
    ctx.arc(x, y, 64 * rate, 0, 2 * Math.PI)
    ctx.clip()
    ctx.drawImage(imageUrl[0], 0 * rate, 74 * rate, 128 * rate, 128 * rate)
    ctx.closePath()
    ctx.restore()
    // 绘制文本
    drawText(ctx, '#1D1E1F', '来自xxx 的脱单团', 66 * rate, 24 * rate, 12)
    ctx.save()
  • 这里头像可能会压缩,导致很丑

  • drawText 绘制文字方法

// 绘制文本
  const drawText = (ctx, color, text, x, y, font = 16) => {
    ctx.setFontSize(font)
    ctx.setFillStyle(color)
    ctx.setTextAlign('left')
    ctx.fillText(text, x, y)
    ctx.stroke()
    ctx.closePath()
  }

画圆

// 画外圆
    ctx.beginPath()
    ctx.arc(56 * rate, 140 * rate, 80 * rate, 0, 2*Math.PI)
    ctx.lineWidth = 16 * rate
    ctx.clip()
    ctx.strokeStyle = "#FFE04A";
    ctx.stroke()
    ctx.closePath()

二维码

  • 这里的二维码目前是写死的,但业务需要动态生成,等我做了加上待更新...
  • 更新了这里,可以去看一下 小程序动态生成二维码
    const size14 = 14 * rate
    // 绘制二维码
    ctx.drawImage(imageUrl[1], 210 * rate, 120 * rate, 86 * rate, 86 * rate)
    drawText(ctx, '#1D1E1F', '扫码认识Ta', 216 * rate, 220 * rate, size14)

最后生成图片

  • 这里微信的官方说,放到 ctx.draw() 的 callback 里面,但是没有执行,不知道为啥,这里就先使用了 setTimeout 模拟异步生成
   setTimeout(() => {
      Taro.canvasToTempFilePath({
        x:0,
        y:0,
        width,
        height,
        canvasId: 'shareuser',
        success: (result) => {
          SetImage(result.tempFilePath)
        },
        fail: (err) => {
          Taro.showToast('图片生成失败!')
        }
      })
    }, 600)

保存到相册中,需要用户授权

  • 调用授权,需要兼容如果用户点击取消的操作
// 保存到相册
  const onClickSaveImage = () => {
    Taro.getSetting({
      success(res) {
        // 如果没有授权过,则要获取授权
        if (!res.authSetting['scope.writePhotosAlbum']) {
          Taro.authorize({
            scope: 'scope.writePhotosAlbum',
            success() {
              // 保存图片
              savePictureSystem()
            },
            fail() { // 用户拒绝
              Taro.showModal({
                title: '授权',
                content: '您拒绝了授权请求,是否要手动开启?',
                success: function (res) {
                  if (res.confirm) {
                    Taro.openSetting({
                      success: function (res) {
                        console.log(res.authSetting)
                        res.authSetting = {
                          "scope.userInfo": true,
                          "scope.userLocation": true
                        }
                      }
                    })
                  } else if (res.cancel) {
                    Taro.showToast({
                      title: '保存失败!',
                      icon: 'close',
                      duration: 2000
                    })
                  }
                }
              })
            }
          })
        } else { // 如果已经授权过,可以直接保存
          savePictureSystem()
        }
      }
    })
  }
 // 把图片保存到系统中
  const savePictureSystem = () => {
    Taro.saveImageToPhotosAlbum({
      filePath: saveImage,
      success(res) {
        Taro.showToast({
          title: '保存成功!'
        })
      },
      fail() {
        Taro.showToast({
          title: '保存失败!',
          icon: 'close',
          duration: 2000
        })
      }
    })
  }

所有代码

// 获取基本信息
  useEffect(() => {
    const d = Taro.getSystemInfoSync()
    const w = d.windowWidth * 0.85
    const h = (w / 0.75).toFixed(2)
    const rate = (d.windowWidth / 375).toFixed(2)
    SetWidth(w)
    SetHeight(h)
    SetRate(rate)

    // 获取所有的网络图片
    Promise.all(
      imageAry.map(img => processMultipleImages(img))
    ).then(images => {
      const imgAll = images.map(i => i.path)
      SetUrl(imgAll)
    })
  }, [])

  useEffect(() => {
    if (imageUrl.length > 0) {
      drawContent()
    }
  }, [imageUrl])

  // 画主体内容
  const drawContent = () => {
    const ctx = Taro.createCanvasContext('shareuser');
    const cx = 5 * rate + 20 * rate
    const cy = 12 * rate + 20 * rate
    // 背景颜色
    ctx.fillStyle = '#fff'
    ctx.fillRect(0, 0, width, height)
    ctx.save()

    // 头像
    ctx.beginPath()
    ctx.arc(cx, cy, 20 * rate, 0, 2 * Math.PI)
    ctx.clip()
    ctx.drawImage(imageUrl[0], 6 * rate, 12 * rate, 40 * rate, 40 * rate)
    ctx.restore()

    drawText(ctx, '#1D1E1F', '来自xxx 的脱单团', 66 * rate, 24 * rate, 12)
    ctx.save()
    // 画外圆
    ctx.beginPath()
    ctx.arc(56 * rate, 140 * rate, 80 * rate, 0, 2*Math.PI)
    ctx.lineWidth = 16 * rate
    ctx.clip()
    ctx.strokeStyle = "#FFE04A";
    ctx.stroke()
    ctx.closePath()
    // 画内圆 并 填充头像
    ctx.beginPath()
    const x = 56 * rate
    const y = 74 * rate + 64 * rate
    ctx.arc(x, y, 64 * rate, 0, 2 * Math.PI)
    ctx.clip()
    ctx.drawImage(imageUrl[0], 0 * rate, 74 * rate, 128 * rate, 128 * rate)
    ctx.closePath()

    ctx.restore()

    // 绘制圆圈装饰
    ctx.beginPath()
    ctx.arc(250 * rate, 47 * rate, 18 * rate, 0, 2*Math.PI)
    ctx.lineWidth = 4 * rate
    ctx.strokeStyle = "#FFE04A";
    ctx.stroke()
    ctx.closePath()

    ctx.beginPath()
    ctx.arc(200 * rate, 80 * rate, 9 * rate, 0, 2*Math.PI)
    ctx.lineWidth = 5 * rate
    ctx.strokeStyle = "#FFE04A";
    ctx.stroke()
    ctx.closePath()

    ctx.beginPath()
    ctx.arc(280 * rate, 98 * rate, 14 * rate, 0, 2*Math.PI)
    ctx.lineWidth = 10 * rate
    ctx.strokeStyle = "#FFE04A";
    ctx.stroke()
    ctx.closePath()

    drawFillCircle(ctx, 238, 83, 9)
    drawFillCircle(ctx, 220, 106, 8)
    drawFillCircle(ctx, 200, 140, 8)

    ctx.restore()

    const size14 = 14 * rate
    // 绘制二维码
    ctx.drawImage(imageUrl[1], 210 * rate, 120 * rate, 86 * rate, 86 * rate)
    drawText(ctx, '#1D1E1F', '扫码认识Ta', 216 * rate, 220 * rate, size14)

    // 绘制个人基本信息
    ctx.beginPath()
    const margin56 = 56 * rate
    drawText(ctx, '#1D1E1F', '某个用户的昵称', size14, 270 * rate, 20 * rate)
    drawText(ctx, '#1D1E1F', '资料', size14, 300 * rate, size14)
    drawText(ctx, '#1D1E1F', '这是个人信息|什么|换行', margin56 * rate, 300 * rate, size14)
    drawText(ctx, '#1D1E1F', '兴趣', size14, 336 * rate, size14)
    drawText(ctx, '#1D1E1F', '唱歌、篮球、rap...', margin56 * rate, 336 * rate, size14)
    drawText(ctx, '#1D1E1F', '简介', size14, 372 * rate, size14)
    drawText(ctx, '#1D1E1F', '这是一段很长的简介...', margin56 * rate, 372 * rate, size14)


    ctx.draw()
    setTimeout(() => {
      Taro.canvasToTempFilePath({
        x:0,
        y:0,
        width,
        height,
        canvasId: 'shareuser',
        success: (result) => {
          SetImage(result.tempFilePath)
        },
        fail: (err) => {
          Taro.showToast('图片生成失败!')
        }
      })
    }, 600)
  }

  // 处理多张网络图片
  const processMultipleImages = (url) => {
    return new Promise((resolve, reject) => {
      Taro.getImageInfo({
        src: url,
        success: (res) => {
          resolve(res)
        },
        fail: () => {
          Taro.showToast({
            title: '生成失败!'
          })
        }
      })
    })
  }

  // 绘制实心圆
  const drawFillCircle = (ctx, x, y, r, w) => {
    ctx.beginPath()
    ctx.arc(x * rate, y * rate, r * rate, 0, 2*Math.PI)
    ctx.fillStyle = "#FFE04A";
    ctx.fill()
    ctx.closePath()
  }

  // 绘制文本
  const drawText = (ctx, color, text, x, y, font = 16) => {
    ctx.setFontSize(font)
    ctx.setFillStyle(color)
    ctx.setTextAlign('left')
    ctx.fillText(text, x, y)
    ctx.stroke()
    ctx.closePath()
  }

  // 保存到相册
  const onClickSaveImage = () => {
    Taro.getSetting({
      success(res) {
        // 如果没有授权过,则要获取授权
        if (!res.authSetting['scope.writePhotosAlbum']) {
          Taro.authorize({
            scope: 'scope.writePhotosAlbum',
            success() {
              savePictureSystem()
            },
            fail() { // 用户拒绝
              Taro.showModal({
                title: '授权',
                content: '您拒绝了授权请求,是否要手动开启?',
                success: function (res) {
                  if (res.confirm) {
                    Taro.openSetting({
                      success: function (res) {
                        console.log(res.authSetting)
                        res.authSetting = {
                          "scope.userInfo": true,
                          "scope.userLocation": true
                        }
                      }
                    })
                  } else if (res.cancel) {
                    Taro.showToast({
                      title: '保存失败!',
                      icon: 'close',
                      duration: 2000
                    })
                  }
                }
              })
            }
          })
        } else { // 如果已经授权过,可以直接保存
          savePictureSystem()
        }
      }
    })
  }

  // 把图片保存到系统中
  const savePictureSystem = () => {
    Taro.saveImageToPhotosAlbum({
      filePath: saveImage,
      success(res) {
        Taro.showToast({
          title: '保存成功!'
        })
      },
      fail() {
        Taro.showToast({
          title: '保存失败!',
          icon: 'close',
          duration: 2000
        })
      }
    })
  }

  return (
    <View className="share-user-container">
      <Canvas style={{width: `${width}px`, height: `${height}px`}} canvasId="shareuser" id="shareuser" className="canvas-wrapper"></Canvas>
      <Button onClick={onClickSaveImage}>保存到相册</Button>

    </View>
  )
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容