图像混合变形-Morpher


这个库很有意思!为什么这么说,因为学会使用这个库,能让你更了解美图,PS的工作原理,前端也能满足修图的需求,接下来就看看这个库有什么作用吧。

一、使用介绍

该库的作者因为是七年前开发的,所以源代码涉及一些现在少用的技术,例如jquery。

源库demo链接附上

使用它,你需要准备变化前及变化后的照片,并且为图片定义三角面的点位,什么是三角面我后面会介绍。

那么首先我们把它用简单的原生js实现一下:

<div class="box">
    <canvas id="myCanvas" width="240" height="295"></canvas>
</div />
let json = { "images": [{ "points": [{ "x": 139, "y": 27 }, { "x": 169, "y": 46 }, { "x": 177, "y": 0 }, { "x": 194, "y": 34 }, { "x": 237, "y": 3 }, { "x": 237, "y": 36 }, { "x": 105, "y": 112 }, { "x": 219, "y": 87 }, { "x": 206, "y": 164 }, { "x": 44, "y": 208 }, { "x": 18, "y": 54 }, { "x": 68, "y": 22 }, { "x": 126, "y": 220 }, { "x": 0, "y": 137 }, { "x": 98, "y": 14 }, { "x": 88, "y": 11 }, { "x": 130, "y": 17 }, { "x": 149, "y": 22 }, { "x": 174, "y": 43 }, { "x": 202, "y": 61 }], "src": "https://pic.imgdb.cn/item/6549975dc458853aef529ced.png" }, { "points": [{ "x": 103, "y": 85 }, { "x": 125, "y": 86 }, { "x": 98, "y": 15 }, { "x": 130, "y": 40 }, { "x": 145, "y": 0 }, { "x": 159, "y": 23 }, { "x": 108, "y": 172 }, { "x": 215, "y": 122 }, { "x": 211, "y": 209 }, { "x": 126, "y": 294 }, { "x": 20, "y": 212 }, { "x": 23, "y": 141 }, { "x": 187, "y": 266 }, { "x": 49, "y": 274 }, { "x": 29, "y": 102 }, { "x": 0, "y": 135 }, { "x": 8, "y": 69 }, { "x": 96, "y": 52 }, { "x": 127, "y": 68 }, { "x": 229, "y": 99 }], "src": "https://pic.imgdb.cn/item/6549975dc458853aef529d1b.png" }], "triangles": [[5, 3, 2], [2, 4, 5], [6, 1, 0], [6, 1, 7], [8, 7, 6], [10, 11, 6], [8, 6, 12], [9, 6, 12], [9, 6, 13], [10, 6, 13], [11, 6, 14], [0, 6, 14], [15, 14, 11], [16, 14, 15], [0, 14, 16], [0, 3, 17], [2, 3, 17], [17, 16, 0], [1, 0, 18], [3, 0, 18], [19, 7, 1], [1, 18, 19]] };

let morpher = new Morpher(json);
morpher.setCanvas(document.querySelector('#myCanvas'));
morpher.set([1, 0]);
morpher.animate([0, 1], 2000);

图片提供的points是该图片的变形点位,三角面:triangles,设置的是points数组对应的下标,连接每一个点位形成三角面,包括图片四个面的三角也需要连接。简单理解如下图:

那么我打的点坐标就是图片需要的points数组,三角面就是把每个三角按逆时针坐标系写出来,包括与图片四个边形成的三角面也要写,这个很重要!

二、应用场景

那么回到我的需求上,我需要在小程序上实现一个侧脸修图的功能,如果你有下巴后缩或者嘴唇前凸后缩等问题,那么我就需要给你矫正一个好看的图片。

为了阅读方便,我将数据优化了一下写法:

  let originData = {
     src: "/images/sidegril.jpg",
     points: {
       0: { x: 0, y: 0 },
       1: { x: 474, y: 0 },
       2: { x: 0, y: 567 },
       3: { x: 474, y: 567 },
       4: { x: 34.54933333333334, y: 298.02227171492206 },
       5: { x: 57.72266666666667, y: 319.48997772828506 },
       6: { x: 56.879999999999995, y: 340.11581291759467 },
       7: { x: 92.46561142944334, y: 353.78112282818836 },
       8: { x: 61.51466666666666, y: 365.3719376391982 },
       9: { x: 70.784, y: 382.630289532294 },
       10: { x: 68.256, y: 409.1492204899777 },
       11: { x: 74.99733333333333, y: 418.4097995545657 },
       12: { x: 118.39466666666667, y: 420.93541202672606 },
       13: { x: 0, y: 298.02227171492206 },
       14: { x: 0, y: 382.630289532294 },
       15: { x: 146.56016508443196, y: 325.9272504262775 },
       16: { x: 155.19749841776527, y: 375.38716133941784 },
     },
     triangles: {
       0: [15, 16, 1],
       1: [15, 0, 4],
       2: [15, 4, 5],
       3: [15, 5, 6],
       4: [15, 6, 7],
       5: [15, 7, 16],
       6: [16, 7, 8],
       7: [16, 8, 9],
       8: [16, 9, 10],
       9: [16, 10, 11],
       10: [16, 11, 12],
       11: [16, 12, 3],
       12: [13, 5, 4],
       13: [13, 6, 5],
       14: [6, 8, 7],
       15: [14, 8, 6],
       16: [14, 9, 8],
       17: [14, 10, 9],
       18: [14, 11, 10],
       19: [11, 2, 12],
       20: [0, 15, 1],
       21: [16, 3, 1],
       22: [12, 2, 3],
       23: [11, 14, 2],
       24: [6, 13, 14],
       25: [13, 4, 0],
     },
   };

const origin_data = this.transformJson(originData);

function transformJson(params) {
   const { points, triangles, src } = params;

   let points1_arr = Object.keys(points).map((key) => points[key]);
   const triangles_arr = Object.keys(triangles).map((key) => triangles[key]);

   // 拷贝出points2_arr
   let points2_arr = JSON.parse(JSON.stringify(points1_arr));

   let noseLipToMove = this.adjustAngleDegrees(
     points2_arr[4],
     points2_arr[5],
     points2_arr[6],
     115
   );
   noseLipToMove.x = noseLipToMove.x * -1;
   // 嘴唇关键点同时移动
   points2_arr[6] = {
     x: points2_arr[6].x + noseLipToMove.x,
     y: points2_arr[6].y + noseLipToMove.y,
   };
   points2_arr[7] = {
     x: points2_arr[7].x + noseLipToMove.x,
     y: points2_arr[7].y + noseLipToMove.y,
   };
   points2_arr[8] = {
     x: points2_arr[8].x + noseLipToMove.x,
     y: points2_arr[8].y + noseLipToMove.y,
   };
   points2_arr[9] = {
     x: points2_arr[9].x + noseLipToMove.x,
     y: points2_arr[9].y + noseLipToMove.y,
   };

   // 计算颏唇角 采取关键点
   const jawLip_p8 = points2_arr[8];
   const jawLip_p9 = points2_arr[9];
   const jawLip_p10 = points2_arr[10];

   let jawLipToMove = this.adjustAngleDegrees(
     points2_arr[8],
     points2_arr[9],
     points2_arr[10],
     140
   );

   jawLipToMove.x = jawLipToMove.x * -1;

   // 根据颏唇角移动距离移动下巴关键点
   points2_arr[10] = {
     x: points2_arr[10].x + jawLipToMove.x,
     y: points2_arr[10].y + jawLipToMove.y,
   };
   points2_arr[11] = {
     x: points2_arr[11].x + jawLipToMove.x,
     y: points2_arr[11].y + jawLipToMove.y,
   };
   points2_arr[12] = {
     x: points2_arr[12].x + jawLipToMove.x,
     y: points2_arr[12].y + jawLipToMove.y,
   };

   const images = [{ points: points1_arr }, { points: points2_arr }];
   return {
     images,
     triangles: triangles_arr,
     src,
   };
 },
 calculateAngle(p1, p2, p3) {
   const vector1 = { x: p2.x - p1.x, y: p2.y - p1.y };
   const vector2 = { x: p3.x - p1.x, y: p3.y - p1.y };
   const dotProduct = vector1.x * vector2.x + vector1.y * vector2.y;
   const length1 = Math.sqrt(vector1.x ** 2 + vector1.y ** 2);
   const length2 = Math.sqrt(vector2.x ** 2 + vector2.y ** 2);
   const angleInRadians = Math.acos(dotProduct / (length1 * length2));
   const angleInDegrees = (angleInRadians * 180) / Math.PI;
   return angleInDegrees;
 },
 adjustAngleDegrees(p2, p1, p3, angle) {
   const targetAngleRadians = (angle * Math.PI) / 180;

   const currentAngleDegrees = this.calculateAngle(p1, p2, p3);
   const currentAngleRadians = (currentAngleDegrees * Math.PI) / 180;

   const vector2 = { x: p3.x - p1.x, y: p3.y - p1.y };
   const length2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y);
   const direction2 = Math.atan2(vector2.y, vector2.x);

   const rotationAngleRadians = targetAngleRadians - currentAngleRadians;

   const adjustedLength = length2; // 使用 length2 以保持长度相等
   const adjustedDirection = direction2 + rotationAngleRadians;

   const adjustedP3X = p1.x + adjustedLength * Math.cos(adjustedDirection);
   const adjustedP3Y = p1.y + adjustedLength * Math.sin(adjustedDirection);

   const adjustedP3 = { x: adjustedP3X - p3.x, y: adjustedP3Y - p3.y };

   return adjustedP3;
 },

transformJson用于将可阅读格式转换为我们需要的格式,并且把需要的角度,比如:鼻唇角、颏唇角,修正为我传递的参数,在新生成的points里面就是配置好需要变化的数组。

直接运行就可以实现我们看到的效果啦


文章作者: feico
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 feico !
评论
  目录