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

推荐阅读更多精彩内容

  • PixiJS是一个2D渲染引擎,能自动侦测并使用WebGL或Canvas。 PixiJS使用JavaScript或...
    JunChow520阅读 12,988评论 4 10
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,693评论 2 59
  • 一、Unity简介 1. Unity界面 Shift + Space : 放大界面 Scene界面按钮渲染模式2D...
    MYves阅读 8,177评论 0 22
  • 在这个看脸的时代,无论真实社交,还是网络社交(特别是陌生人社交),甚至虚拟形象社交,“颜值即正义”。长得好看是一大...
    停停走走UP阅读 2,483评论 0 11
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,358评论 0 17