前言

前段时间网上冲浪的时候发现一个好玩的网站,就这个https://upuptoyou.com/,有意思的是在选择 play 的时候会填写一句话,输入的文字会创建一张图片里面的小人会举起手来,眼前一亮,有点意思。想着自己能不能也搞一个玩玩,之前项目紧,这次放假花两天时间整一个。

需求分析

拿到东西之后要学会分析接下来需要做的功能项,包括要处理的细节和参数以及可能多的一些扩展。

  1. 需要随机的举牌小人图片
  2. 需要在输入的时候实时生成随机的图片,然后把文字赋值到图片上
  3. 输入的文字可以是空格或者回车,空格处图片格开一张,回车图片换行排列
  4. 支持可配置的文字颜色,背景颜色,画布大小或者自适应画布
  5. 支持可配置的 json 文件以便扩展其他小人(这个暂时还没做)

开始准备

一开始想着网上找找有没有类似的项目,但是找了半天也没有差不多的,有的话也是样式不满意和排列不对,没找到合适的之后就开始自己动手啦,虽然没有找到满意的项目,但是我找到了人家项目里面的举牌小人了,拿来用一下不过分吧(小声..)。

创建一个画布开始

关于如何写一个简单的插件我之前的一篇文章就写到过了https://juejin.cn/post/6987677993380511751,这次就拿来用,先从创建画布开始;

var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d');
// 创建一个默认的画布大小
canvas.width = 1920;
canvas.height = 980;

获取输入框内的文本针对换行符 \n 做拆分处理,并且设置监听输入框的 input 事件。

var text = self.params.text.value.split('\n');
this.addEvent(this.params.text, 'input', function (e) {
  self.params.text.innerHTML = e.target.value
  self.refresh()
  // 这里可以做一个截流处理
  // self.throttleDelay(self.refresh.bind(self), 500)
})

再给画布填充一下颜色。

ctx.fillStyle = self.params.bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);

加载图片资源

针对上面拿到 text 数组项,通过循环的方式加载一下图片资源,这里我加载图片的兼容,我都是做回掉的处理,没有使用 Promise 写,如果不考虑那些可以尝试使用 Promise ;这里在写的时候做了一个顺序图片的输出,使用固定索引的方式 imgs[index] 保证输出的图片和里面的文字都是顺序排列的(这个非常重要!),通过随机函数 Math.random() 取到随机的图片然后加载,最后在保证所有的图片加载完成之后,再 callback() 返回出去,这里地方很重要!每次问候选人的时候也是没几个人能回答上。

loadImg: function(urls, callback) {
    var len = 0
    var imgs = []
    urls.forEach(function (item, index) {
    var image = new Image()
    var url = `./images/QP4a5rvW_${Math.floor(Math.random() * 40)}.png`
        image.onload = function () {
      imgs[index] = {
        image: image,
        text: item
      }
      // 保证所有的图片已加载完成
      if (++len === urls.length) {
          callback(imgs)
            }
        }
        image.src = url
    })
}

在 canvas 上画出所有的图片

加载完了图片之后,我们就开始把图片放到刚创建的 canvas 中,在两层遍历之后就拿到了单个文字和随机的图片,然后再去计算图片所需要在的位置;

x 表示图片的横向位移点, y 表示图片的纵向位移点,因为要做的跟网站一样就要有错落的感觉,而且小人的站姿也是45度的排布,所以计算得到横纵坐标。

var imgWidthAndHeight = []
text.forEach(function(item, i) {
  // 加载图片资源
  self.loadImg(item.split(''), function(res) {
    // 关键代码:如果使用了 arr.push(res) 回导致输出的索引跟填写的文字顺序不一致的情况
    // 所以直接使用赋值的方式,固定图片数组输出的位置,这里要保证输出的顺序,
    // 其他情况不需要就可以使用push, 下面的获取图片同理
    arr[i] = res;
    // 保证所有的图片都已加载完成,并且数据都有
    if(++len == text.length) {
      arr.forEach(function(item, index) {
        var distance = arr.length - index - 1
        item.forEach(function(its, idx) {
          var it = its.image
          var tx = its.text
          // 如果是空格就直接返回
          if (tx === ' ') return
          var x = ((idx * it.naturalWidth) - (idx * 35)) + (distance * 50)
          var y = (index * (it.naturalHeight * .3)) + (idx * 20)
          // 把图片的宽高保存
          imgWidthAndHeight.push({
            x: x + it.naturalWidth,
            y: y + it.naturalHeight
          })
          // 在 canvas 上画出所有的图片
          ctx.drawImage(it, x, y, it.naturalWidth, it.naturalHeight)
          // 在 canvas 上画出所有的文字
          ***
        })
      })
    }
  })
})

在 canvas 上画出所有的文字

等上面的图片都加载完了并且也画到了 canvas 上了后,就开始把文字也加上去,在这个阶段碰到了一个坑,弄了很久,如果有知道的小伙伴可以留言告诉我,感激不尽。

一开始的想法是通过文字去建立一个新的文字 canvas ,然后在把文字的 canvas 通过 getImageData 拷贝一份,然后用 putImageData 放到一开始默认新建的画布上,理想是美好的,现实却大脸了,发现弄上去了文字的 canvas 没有透明,导致附在举牌小人的图片上一直会有一个白色的底,不管怎么去做都去不掉,奇怪的是,这个白底还跟默认的 canvas 的背景颜色有关联,在我尝试的使用 .sytle.background 设置默认 canvas 的背景颜色时,文字的 canvas 的背景也会变成刚才设置的颜色,奇妙。索性我放弃了使用 canvas 覆盖,直接用了转换成 png 的图片再通过 drawImage 的方式把图片画上去,虽然七走八绕的花了时间,但是也做出来了效果。

drawText: function(word, width, height, callback) {
  var canvas = document.createElement('canvas')
  canvas.width = width
  canvas.height = height
  var ctx = canvas.getContext('2d')

  ctx.transform(0.766044, 0.3, -0.742788, 0.766044, 20, 0)
  ctx.font = '30px SimHei'
  ctx.textAlign = 'center'
  ctx.fillStyle = this.params.color
  ctx.fillText(word, 14, 28)

  // 这里本来设置是返回的 getImageData 的,但是发现在使用canvas 
  // 堆叠到另一个canvas中背景一直设置不到的透明
  // 因为另一个的canvas中添加了一个图片,而图片是颜色的,会导致文字的canvas一直是有底色的
  // 没办法,索性使用导出图片的方式保证透明,如果是canvas就不能保证是透明
  var img = new Image();
  img.onload = function () {
    callback(img)
  }
  img.src = canvas.toDataURL('image/png');
}

等所有的文字画完之后再输出图片

最后要做的便是把所有画好的图片重新输出一张图片即可。这里就是要比较所有的图片出现图片的具体位置,得到的横纵坐标来比较大小,在前面加载图片资源的时候就保存了图片的所有的宽高,这里我们就只需要取出来就可以做比较啦!找到最大的 xy 之后重新赋值默认的 canvas 画布大小即可,注意的点是,如果之前的画布过小再重新赋值大小的时候会出现小人被截掉的情况,所以在一开始我就把默认的画布设置成 1920 * 980 大小,这样就不会出现上述问题了,还有就是如果直接动态设置 canvas 的宽高时,会导致里面的所有内容被清除,这里处理的方式也很简单,就是用 getImageData 保存一下图片内容信息,设置宽高后在重新 putImageData 就可以了。

// 4. 等所有的文字画完之后再输出图片
if (++len === text.join('').length) {
  var width = Math.max.apply(null, imgWidthAndHeight.map(function(item) {
    return item.x
  }))
  var height = Math.max.apply(null, imgWidthAndHeight.map(function(item) {
    return item.y
  }))
  // 改变大小前用getImageData保存图像
  var copyCanvas = ctx.getImageData(0, 0, canvas.width, canvas.height)
  canvas.width = width;
  canvas.height = height;
  // 改变了宽高之后再重新设置之前的图像
  ctx.putImageData(copyCanvas, 0, 0);
  // 设置头像之后再输出图片
  self.loadLogo(ctx, function() {
    var dataImg = new Image();
    dataImg.src = canvas.toDataURL('image/png');
    callback(dataImg);
  })
}

最后在拼上我们的站点logo 输出图片就可以咯。
预览 地址
以上完成代码之后就可以先试试效果怎么样,代码已上传Github,觉得还不错的给个小赞支持一下