fabric.js实现一个头套人物定制插件

1,493 阅读2分钟

核心函数

创建画布

因为我们身体模版的宽高为500x660,所以我们画布也设置成这个大小

<canvas id="canvas"></canvas>
import { fabric } from "fabric";
const instance = new fabric.Canvas('canvas', {
    width: 500,
    height: 660
});

添加身体

// 添加背景
function addBackground(url) {
    return new Promise((resolve) => {
        fabric.Image.fromURL(url, img => {
            img.scaleToWidth(500);
            instance.setBackgroundImage(img, instance.renderAll.bind(instance));
            resolve();
        }, {
            crossOrigin: 'Anonymous'
        });
    })
}

添加头像

// 添加虚拟头像
function addVirtualAvatar(url) {
    return new Promise((resolve) => {
        fabric.Image.fromURL(url, img => {
            img.scaleToWidth(110);
            img.set({
                top: 180,
                left: 200
            });
            instance.add(img);
            resolve();
        }, {
            crossOrigin: 'Anonymous'
        });    
    })
    
}

好像已经可以了?!我们看看效果!

// 添加背景
await addBackground(bgURL);
// 添加头像
await addVirtualAvatar(avatarURL);
// 刷新画布
instance.renderAll();

1640164449593.jpg

看着有点奇怪,我们的头部超出了黑色区域!这不是我们想要的效果。

查遍所有参考资料之后,终于找到一个解决方案:globalCompositeOperation

globalCompositeOperation(混合模式)

  • 绘制对象为粉色方块
  • 现有画布为蓝色方块

列举几种常见模式:

1. source-over

默认模式,绘制对象在现有画布之上,且全部显示。

20141211105038312.png

2. destination-over

绘制对象在现有画布之下,且全部显示。

20141211105044707.png

3. source-atop

绘制对象在现有画布之上,但是只显示与现有画布重叠部分。

20141211105052725.png

4. destination-atop

绘制对象在现有画布之下,但是只显示与现有画布重叠部分。

20141211105059698.png

现在我们就有思路了,如果我们先绘制一个黑色区域部分,然后再绘制头部,并且将头部设置为source-atop,这样我们的头就不会超出去了。

代码改造

头部改造

function addVirtualAvatar(url) {
    return new Promise((resolve) => {
        fabric.Image.fromURL(url, img => {
            img.scaleToWidth(110);
            img.set({
                top: 180,
                left: 200,
                globalCompositeOperation: 'source-atop'
            });
            instance.add(img);
            resolve();
        }, {
            crossOrigin: 'Anonymous'
        });    
    })
    
}

绘制一个SVG遮盖层

function addSVG(url) {
    return new Promise((resolve) => {
        fabric.loadSVGFromURL(url, (objects, options) => {
            const mask = fabric.util.groupSVGElements(objects, options);
            const scale = 500 / 3100;
            mask.scale(scale).set({
                left: mask.left * scale,
                top: mask.top * scale,
            })
            instance.add(mask);
            resolve(mask);
        });
    })
}

我们绘制遮盖层和头像试试。

// 添加遮盖
await addSVG(SVGURL);
// 添加头像
await addVirtualAvatar(avatarURL);
// 刷新画布
instance.renderAll();

1640166763333.jpg

好像达到效果了,那我们再添加背景就可以搞定了 >.<。

// 添加遮盖
await addSVG(SVGURL);
// 添加头像
await addVirtualAvatar(avatarURL);
// 添加背景
await addBackground(bgURL);
// 刷新画布
instance.renderAll();

1640164449593.jpg

奇怪了,加上背景之后又变成这样了。但是头部超出身体的部分被隐藏了。

思考后应该是黑色区域和背景图先绘制了被当成了一个整体,然后再绘制的头,这样头部就不会超出身体。

各种实验之后,终于找到了解决办法,不设置backgroundImage,改成设置overlayImage,然后将overlayImageglobalCompositeOperation属性设置为destination-over

使用overlayImage代替backgroundImage

function addOverlay(url) {
    return new Promise((resolve) => {
        fabric.Image.fromURL(url, img => {
            img.scaleToWidth(500);
            img.set({
                globalCompositeOperation: 'destination-over'
            })
            instance.setOverlayImage(img, instance.renderAll.bind(instance));
            resolve();
        }, {
            crossOrigin: 'Anonymous'
        });
    })
}
// 添加遮盖
await addSVG(SVGURL);
// 添加头像
await addVirtualAvatar(avatarURL);
// 添加背景
await addOverlay(bgURL);
// 刷新画布
instance.renderAll();

结果

终于完成了!

在线展示

codepen地址

完整代码

<!--
 * @Description:
 * @Version: 2.0
 * @Author: Yaowen Liu
 * @Date: 2021-12-23 09:53:03
 * @LastEditors: Yaowen Liu
 * @LastEditTime: 2021-12-23 10:07:28
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <canvas id="canvas"></canvas>

  <script src="http://fabricjs.com/lib/fabric.js"></script>
  <script>

    const AVATAR_URL = 'https://cdn.shopifycdn.net/s/files/1/0343/0275/4948/files/vFace.png?v=1634089968';
    const BODY_URL = 'https://cdn.shopifycdn.net/s/files/1/0510/1423/8379/files/1598957ed24441c515af24f3bc5faac2_e7e99901-8ca6-48d6-b483-7c218e9703fd.png?v=1623061440';
    const SVG_URL = 'https://cdn.shopifycdn.net/s/files/1/0510/1423/8379/files/c9ab3de818e54ee83338537021b4e538_85035b24-be5e-4184-aefb-e2de3ef48312.svg?v=1623061439';

    // 实列化
    const instance = new fabric.Canvas('canvas', {
      width: 500,
      height: 660
    });

    // 添加头
    function addVirtualAvatar(url) {
      return new Promise((resolve) => {
        fabric.Image.fromURL(url, img => {
          img.scaleToWidth(110);
          img.set({
            top: 180,
            left: 200,
            globalCompositeOperation: 'source-atop'
          });
          instance.add(img);
          resolve();
        }, {
          crossOrigin: 'Anonymous'
        });
      })
    }

    // 添加遮盖
    function addSVG(url) {
      return new Promise((resolve) => {
        fabric.loadSVGFromURL(url, (objects, options) => {
          const mask = fabric.util.groupSVGElements(objects, options);
          const scale = 500 / 3100;
          mask.scale(scale).set({
            left: mask.left * scale,
            top: mask.top * scale,
          })
          instance.add(mask);
          resolve();
        });
      })
    }

    // 添加身体
    function addOverlay(url) {
      return new Promise((resolve) => {
        fabric.Image.fromURL(url, img => {
          img.scaleToWidth(500);
          img.set({
            globalCompositeOperation: 'destination-over'
          })
          instance.setOverlayImage(img, instance.renderAll.bind(instance));
          resolve();
        }, {
          crossOrigin: 'Anonymous'
        });
      })
    }

    // 渲染
    async function renderer() {
      // 添加遮盖
      await addSVG(SVG_URL);
      // 添加头像
      await addVirtualAvatar(AVATAR_URL);
      // 添加背景
      await addOverlay(BODY_URL);
      // 刷新画布
      instance.renderAll();
    }

    renderer();
  </script>
</body>

</html>