从0到1实现基于Pixi js的Spine头像生成功能

项目背景

这个项目是给每个用户提供了一个avatar动态形象。用户可以任意搭配头发、衣服等部件。要求在在用户保存形象的同时,生成头像截图,头像背景需要自定义,跟动态形象不是同一个背景。

u06x0-gaw11.gif

技术背景

整体采用Pixi js + Spine + Vue的技术栈

技术难点

一 用户形象是动画,如果直接截屏,会导致每次截取的形象不同,而且可能出现闭眼的情况。

比如下面的截图:


image.png

二 给生成的头像动态添加背景

avatar动态形象的背景是透明的,生成头像需要特定的背景,这个背景肯定是不能直接添加在avatar形象上的,需要在截图的时候动态添加背景。

尝试的解决方案

一 初始化一个opacity为0的avatar动态形象,对其进行截图

在用户保存形象时,初始化一个opacity为0的avatar动态形象,不播放动画,让其停留在第一帧,对其进行截图。就不会出现闭眼的问题。同时还可以直接给avatar添加一个背景,这样截取的头像就会带有背景。解决了上面一、二两个难点。
存在问题:从新的avatar形象被初始化开始,页面开始严重掉帧,出现了性能问题。
这个作为保底方案,开始探索轻量级的截图方案。

截取的头像可能会闭眼是截图时动画播放到哪一帧不能控制导致的。动画第一帧是正常帧,头像截取都会需要针对这一帧来操作。

二 缓存每次动画开始的第一帧

缓存动画循环播放的第一帧,如果用户在本次动画循环中保存了形象,那么直接对缓存的第一帧进行头像裁剪。
问题在于,用户在动画第一帧播放之后形象是可以被改变的,这个方案被pass掉

确定的解决方案

选择的解决方案是用户保存形象时,立刻开始从第一帧播放动画。
这个是最终采用的方案,下面介绍实现细节。

一 截取当前帧

const avatarWidth = this.shotOption.area[2] * ratio
    const renderer = app.renderer
    const stage = app.stage
    let matrix = new PIXI.Matrix()
    matrix = matrix.translate(-this.shotOption.area[0], -this.shotOption.area[1])
    const renderTexture = PIXI.RenderTexture.create(avatarWidth, avatarWidth)

    renderer.render(stage, { renderTexture, transform: matrix })
    const canvas = renderer.plugins.extract.canvas(renderTexture)

使用Pixi的RenderTexture储存当前Canvas的数据,即动画第一帧数据,对头像大小和裁剪的区域有要求,可以透过Pixi的Matrix来配置。将RenderTexture渲染到新的Canvas。

二 添加头像背景

addCanvasBg(_ctx: any, _bgColor: string) {
    const tmpCtx = document.createElement('canvas').getContext('2d')
    if (!tmpCtx) {
      return null
    }
    tmpCtx.canvas.width = _ctx.canvas.width
    tmpCtx.canvas.height = _ctx.canvas.height
    tmpCtx.fillStyle = _bgColor
    tmpCtx.fillRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height)
    tmpCtx.drawImage(_ctx.canvas, 0, 0)

    return tmpCtx.canvas
  }

const newCanvas = this.addCanvasBg(canvas.getContext('2d'), this.shotOption.backgroundColor)
return newCanvas ? newCanvas.toDataURL('image/jpeg') : canvas.toDataURL('image/jpeg')

构建新的Canvas,绘制背景,将截取的头像绘制在新的Canvas上,输出最终的头像


91624781338_.pic.jpg

解决生成的头像在高清屏上不清晰的问题

上面是在小米11的截图,可以发现生成的头像有点模糊。
这里面涉及到css虚拟像素和设备真实像素的概念。
假设给Canvas style样式设置的宽高是100px X 100px,在dpr为3的设备上,渲染这个Canvas使用的真实像素点是300 X 300,如果Canvas内容设置的宽高是100 X 100,那么内容就会被拉伸。看起来会变模糊。
最终分析源码发现在截取avatar第一帧图片时传入是css虚拟像素的宽高,是需要手动添加dpr,否则默认为1,就导致截取的图片像素点变少了,后面构建Canvas内容宽高也是采用这个宽高,最终导致生成的头像变模糊。
需要将

const renderTexture = PIXI.RenderTexture.create(avatarWidth, avatarWidth)

改为

const renderTexture = PIXI.RenderTexture.create(avatarWidth, avatarWidth, PIXI.SCALE_MODES.LINEAR, dpr)

效果如下:


111624784313_.pic.jpg

截图完整代码:

  public shotScreen(isCurrent = true): string | null {
    if (!this.enableShot) {
      throw new Error('截屏功能未开启!')
    }

    if (!isCurrent) {
      this.playAnimation(this.animationName)
    }

    const avatarWidth = this.shotOption.area[2] * ratio
    const renderer = app.renderer
    const stage = app.stage
    let matrix = new PIXI.Matrix()
    matrix = matrix.translate(-this.shotOption.area[0], -this.shotOption.area[1])
    const renderTexture = PIXI.RenderTexture.create(avatarWidth, avatarWidth, PIXI.SCALE_MODES.LINEAR, dpr)

    renderer.render(stage, { renderTexture, transform: matrix })
    const canvas = renderer.plugins.extract.canvas(renderTexture)
    const newCanvas = this.addCanvasBg(canvas.getContext('2d'), this.shotOption.backgroundColor)
    if (app) {
      return newCanvas ? newCanvas.toDataURL('image/jpeg') : canvas.toDataURL('image/jpeg')
    }
    return null
  }

  addCanvasBg(_ctx: any, _bgColor: string) {
    const tmpCtx = document.createElement('canvas').getContext('2d')
    if (!tmpCtx) {
      return null
    }
    tmpCtx.canvas.width = _ctx.canvas.width
    tmpCtx.canvas.height = _ctx.canvas.height
    tmpCtx.fillStyle = _bgColor
    tmpCtx.fillRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height)
    tmpCtx.drawImage(_ctx.canvas, 0, 0)

    return tmpCtx.canvas
  }

后续优化方向

我们目前截图功能是放在客户端实现的,就导致用户在点击保存形象之后,会存在截图->通过原生桥将base64保存到文件服务器->将原生返回的文件url提交到后台 完成整个流程,就导致整个流程比较长。
未来期望把截图上传这块功能放到node层来处理。
目前预想的方案有两个:

  1. 采用pupteer无头浏览器运行一个包含静止avatar形象网页,然后对网页截图再通过rpc提交到java后台。
  2. 使用jsdom,node-canvas,尝试直接在node运行pixi, pixi-spine,实现截图,类似思想的开源方案有pixi-shim。

你可能感兴趣的:(从0到1实现基于Pixi js的Spine头像生成功能)