九宫格图片合成算法在微信小程序中的使用

在上一篇文章《web中九宫格图片合成算法的探究》中,我们讨论了九宫格图片合成算法的原理以及在网页中的使用,本文的主要目的就是讲上一篇文章写的合成方法迁移到小程序中,让我们在小程序中也可以使用九宫格来合成图片。

原理在上一篇文章中已经介绍过了,核心思想都是一样的,这里就不再赘述,我们直接看迁移过程,其实所谓迁移就是讲之前代码中小程序不支持的方法改成小程序支持的,主要有以下几个点:

1、加载图片的方法,在web中我们可以通过直接new一个Image对象的方式来加载图片,但是这种方式在小程序中并不被支持,所以我们将加载图片的方法改成下面的代码:

/* ---------------images的相关方法--------------------------*/
/**
 * 加载图片
 * @param  {string} url 待加载图片的地址
 * @param  {Function} cb 加载完成后的回调
 */
function loadImage(url, cb) {
  const promise = new Promise((resolve, reject) => {
    if (!url) {
      reject();
    } else {
      wx.getImageInfo({
        src: url,
        success: res => {
          resolve(res);
        },
        fail: reject
      });
    }
  });

  return promise;
}

2、在网页中我们通过动态创建canvas元素来合成素材,但是在微信小程序中,我们不能动态创建元素,所以我们需要将页面或组件中的canvas元素或者绘图环境传入。这里改动了合成主方法combine,让其接受绘图环境。

/**
 * 根据9张小图片, 拼接成一张指定宽高的大图.
 * @param  {array} images 所有小图的地址的数组
 * @param  {number} targetWidth 拼接后的图片的宽
 * @param  {number} targetHeight 拼接后的图片的高
 * @param  {object} paddings 图片白边:包括top, right, bottom, left四个方向的值.
 * @param  {function} 拼接完成后的回调, 参数里包含了拼接后的大图.
 */
export const combine = (
  ctx,
  images,
  targetWidth,
  targetHeight,
  paddings,
  outPaddings,
  cb
) => {
  targetWidth = parseInt(targetWidth) || 0;
  targetHeight = parseInt(targetHeight) || 0;

  paddings.top = parseInt(paddings.top) || 0;
  paddings.right = parseInt(paddings.right) || 0;
  paddings.bottom = parseInt(paddings.bottom) || 0;
  paddings.left = parseInt(paddings.left) || 0;

  const oPaddings = outPaddings || {
    top: 0,
    left: 0,
    right: 0,
    bottom: 0
  };


  // 参数检查.
  if (!images || images.length !== 9 || !targetWidth || !targetHeight) {
    return;
  }

  // 获取所有图片的信息, 包括宽和高.
  init(images, allImages => {
    // 计算每一张小图在canvas绘制时的坐标和宽高
    const imagesOptionsInCanvas = computedImageParamsInCanvas(
      allImages,
      targetWidth,
      targetHeight,
      paddings,
      oPaddings
    );

    // 新建一个新的canvas作为绘图容器
    const { container } = imagesOptionsInCanvas;

    // 把图片绘制到canvas上.
    imagesOptionsInCanvas.images.forEach(imgOption => {
      drawImage(
        ctx,
        imgOption.img.path,
        imgOption.x,
        imgOption.y,
        imgOption.img.width,
        imgOption.img.height,
        imgOption.dWidth,
        imgOption.dHeight
      );
    });

    ctx.draw(false, cb);
  });
};

注意这里并没有返回合成好的图片链接或者base64,只是通知父组件素材已经绘制好,父组件可以在回调函数中调用wx.canvasToTempFilePath方法获取合成素材的临时路径,当然也可以在这里直接返回临时路径,不过需要多接受一个参数canvasId,这个实现也相对简单,这里就不再多说,有兴趣的可以自行改动。

完整的算法代码:

/* ---------------images的相关方法--------------------------*/
/**
 * 加载图片
 * @param  {string} url 待加载图片的地址
 * @param  {Function} cb 加载完成后的回调
 */
function loadImage(url, cb) {
  const promise = new Promise((resolve, reject) => {
    if (!url) {
      reject();
    } else {
      wx.getImageInfo({
        src: url,
        success: res => {
          resolve(res);
        },
        fail: reject
      });
    }
  });

  return promise;
}

/**
 * 计算图片的宽和高
 * @param  {string} url 待加载图片的地址
 * @param  {Function} cb 加载完成后的回调
 */
function getImageSize(url, cb) {
  const size = {
    width: 0,
    height: 0
  };

  if (!url) {
    cb && cb(size);
  } else {
    loadImage(url).then(img => {
      size.width = img.width;
      size.height = img.height;

      cb && cb(size);
    });
  }
}
/* ---------------end images的相关方法------------------------*/

/* ---------------计算的相关方法------------------------*/
/**
 * 根据9张小图, 计算使用这9张小图拼接(不缩放)后的图片的基础宽高.
 * @param  {array} images 包含待拼接的9张小图的数组
 */
function getBaseImageSize(images) {
  const size = {
    width: 0,
    height: 0
  };

  if (images && images.length === 9) {
    size.width = images[0].width + images[1].width + images[2].width;
    size.height = images[0].height + images[3].height + images[6].height;
  }

  return size;
}

/**
 * 获取4个角上的缩放比.
 * @param  {object} baseSize   包含width和height两个属性的对象
 * @param  {object} targetSize  包含width和height两个属性的对象
 */
function getRatio(baseSize, targetSize) {
  const ratio = {
    width: 1,
    height: 1
  };
  if (baseSize && targetSize) {
    const wRatio = baseSize.width / targetSize.width;
    const hRatio = baseSize.height / targetSize.height;

    ratio.width = wRatio;
    ratio.height = hRatio;
  }

  return ratio;
}

/**
 * 计算白边占用整个宽高的比例.
 * @param  {number} padding  白边的像素值
 * @param  {[type]} baseSize 包含width和height两个属性的对象
 */
function getPaddingRatio(paddings, baseSize) {
  return {
    top: paddings.top / baseSize.height,
    right: paddings.right / baseSize.width,
    bottom: paddings.bottom / baseSize.height,
    left: paddings.left / baseSize.width
  };
}

function getPaddingsInContainer(
  targetWidth,
  targetHeight,
  padingTopRatio,
  padingBottomRatio,
  padingLeftRatio,
  padingRightRatio
) {
  const [w, h, tr, br, lr, rr] = [
    targetWidth,
    targetHeight,
    padingTopRatio,
    padingBottomRatio,
    padingLeftRatio,
    padingRightRatio
  ];

  const left =
    (lr * w * (1 - lr) + lr * rr * w) / ((1 - lr) * (1 - lr) + lr * rr);
  const right =
    (rr * w * (1 - rr) + lr * rr * w) / ((1 - rr) * (1 - rr) + lr * rr);
  const top =
    (tr * h * (1 - tr) + tr * br * h) / ((1 - tr) * (1 - tr) + tr * br);
  const bottom =
    (br * h * (1 - br) + tr * br * h) / ((1 - br) * (1 - br) + tr * br);

  return { left, right, top, bottom };
}

/**
 * 计算每张图片在canvas上的坐标和实际要绘制的大小.
 * @param  {array} images 基于Stream的图片数组, 每一对象包含图片的实际数据和图片的各个属性如width, height
 * @param  {number} targetWidth 合成后的图片的宽
 * @param  {number} targetHeight 合成后的图片的高
 * @param  {number} padding 白边的大小.
 */
function computedImageParamsInCanvas(
  images,
  targetWidth,
  targetHeight,
  paddings,
  outPaddings
) {
  const baseSize = getBaseImageSize(images);
  const paddingRatios = getPaddingRatio(paddings, baseSize);
  const outPaddingRatios = getPaddingRatio(outPaddings, baseSize);

  // 期望的宽加上左侧白边的宽
  const paddingsInContainer = getPaddingsInContainer(
    targetWidth,
    targetHeight,
    paddingRatios.top,
    paddingRatios.bottom,
    paddingRatios.left,
    paddingRatios.right
  );

  const outPaddingsInContainer = getPaddingsInContainer(
    targetWidth,
    targetHeight,
    outPaddingRatios.top,
    outPaddingRatios.bottom,
    outPaddingRatios.left,
    outPaddingRatios.right
  );

  const containerWidth = Math.ceil(
    targetWidth + paddingsInContainer.left + paddingsInContainer.right
  );

  // 期望的高加上下宽白边的高.
  const containerHeight = Math.ceil(
    targetHeight + paddingsInContainer.top + paddingsInContainer.bottom
  );
  const containerSize = { width: containerWidth, height: containerHeight };
  const ratio = getRatio(baseSize, containerSize);

  const imagesOptionsInCanvas = {
    container: {
      paddings: paddingsInContainer,
      outPaddings: outPaddingsInContainer,
      width: containerWidth,
      height: containerHeight
    },
    images: []
  };

  const sideRatio = images[0].width / images[0].height;
  let sideWidth = Math.ceil(images[0].width / ratio.width);
  let sideHeight = Math.ceil(images[0].height / ratio.height);
  if (sideWidth > sideHeight * sideRatio) {
    sideWidth = Math.round(sideHeight * sideRatio);
  } else if (sideHeight > sideWidth / sideRatio) {
    sideHeight = Math.round(sideWidth / sideRatio);
  }

  /* 计算小图缩放后的大小 */
  // 左上角
  const topLeftImgSize = {
    width: sideWidth,
    height: sideHeight
  };

  // 右上角
  const topRightImgSize = {
    width: sideWidth,
    height: topLeftImgSize.height
  };

  // 左下角
  const bottomLeftImgSize = {
    width: topLeftImgSize.width,
    height: sideHeight
  };

  // 右下角
  const bottomRightImgSize = {
    width: topRightImgSize.width,
    height: bottomLeftImgSize.height
  };

  // 顶部中间区域
  const topMiddleImgSize = {
    width: containerWidth - (topLeftImgSize.width + topRightImgSize.width),
    height: topLeftImgSize.height
  };

  // 左侧中间区域
  const leftMiddleImgSize = {
    width: topLeftImgSize.width,
    height: containerHeight - (topLeftImgSize.height + bottomLeftImgSize.height)
  };

  // 中间区域
  const middleImgSize = {
    width: topMiddleImgSize.width,
    height: leftMiddleImgSize.height
  };

  // 右侧中间区域
  const rightMiddleImgSize = {
    width: topRightImgSize.width,
    height:
      containerHeight - (topRightImgSize.height + bottomRightImgSize.height)
  };

  // 底部中间区域
  const bottomMiddleImgSize = {
    width: topMiddleImgSize.width,
    height: bottomLeftImgSize.height
  };
  /*--------------------------------------------*/

  // 计算左上角小图的坐标和缩放后的大小.
  const topLeftImg = {
    x: 0,
    y: 0,
    dWidth: topLeftImgSize.width,
    dHeight: topLeftImgSize.height,
    img: images[0]
  };

  // 计算顶部中间小图的坐标和缩放后的大小.
  const topMiddleImg = {
    x: topLeftImg.dWidth,
    y: 0,
    dWidth: topMiddleImgSize.width,
    dHeight: topMiddleImgSize.height,
    img: images[1]
  };

  // 计算右上角张小图的坐标和缩放后的大小.
  const topRightImg = {
    x: topLeftImgSize.width + topMiddleImgSize.width,
    y: 0,
    dWidth: topRightImgSize.width,
    dHeight: topRightImgSize.height,
    img: images[2]
  };

  // 计算左侧中间区域小图的坐标和缩放后的大小.
  const leftMiddleImg = {
    x: 0,
    y: topLeftImgSize.height,
    dWidth: rightMiddleImgSize.width,
    dHeight: rightMiddleImgSize.height,
    img: images[3]
  };

  // 计算中间区域小图的坐标和缩放后的大小.
  const middleImg = {
    x: topLeftImgSize.width,
    y: topLeftImgSize.height,
    dWidth: middleImgSize.width,
    dHeight: middleImgSize.height,
    img: images[4]
  };

  // 计算右侧中间区域小图的坐标和缩放后的大小.
  const rightMiddleImg = {
    x: topLeftImgSize.width + middleImgSize.width,
    y: topLeftImgSize.height,
    dWidth: rightMiddleImgSize.width,
    dHeight: rightMiddleImgSize.height,
    img: images[5]
  };

  // 计算左下角张小图的坐标和缩放后的大小.
  const bottomLeftImg = {
    x: 0,
    y: topRightImgSize.height + leftMiddleImgSize.height,
    dWidth: bottomLeftImgSize.width,
    dHeight: bottomLeftImgSize.height,
    img: images[6]
  };

  // 计算底部中间区域小图的坐标和缩放后的大小.
  const bottomMiddleImg = {
    x: bottomLeftImgSize.width,
    y: topRightImgSize.height + leftMiddleImgSize.height,
    dWidth: bottomMiddleImgSize.width,
    dHeight: bottomMiddleImgSize.height,
    img: images[7]
  };

  // 计算底部右侧区域小图的坐标和缩放后的大小.
  const bottomRightImg = {
    x: bottomLeftImgSize.width + bottomMiddleImgSize.width,
    y: topRightImgSize.height + leftMiddleImgSize.height,
    dWidth: bottomRightImgSize.width,
    dHeight: bottomRightImgSize.height,
    img: images[8]
  };

  // 计算后的添加到数组中并返回
  imagesOptionsInCanvas.images.push(topLeftImg);
  imagesOptionsInCanvas.images.push(topMiddleImg);
  imagesOptionsInCanvas.images.push(topRightImg);
  imagesOptionsInCanvas.images.push(leftMiddleImg);
  imagesOptionsInCanvas.images.push(middleImg);
  imagesOptionsInCanvas.images.push(rightMiddleImg);
  imagesOptionsInCanvas.images.push(bottomLeftImg);
  imagesOptionsInCanvas.images.push(bottomMiddleImg);
  imagesOptionsInCanvas.images.push(bottomRightImg);

  return imagesOptionsInCanvas;
}

/* ---------------end 计算的相关方法------------------------*/

/* ---------------DOM的相关方法------------------------*/

/**
 * 在canvas上绘制图片.
 * @param  {HTMLElement} canvas canvas节点
 * @param  {blob} img 待绘制的图片
 * @param  {number} x 目标画布的左上角在目标canvas上 X 轴的位置
 * @param  {number} y 目标画布的左上角在目标canvas上 Y 轴的位置
 * @param  {number} width 需要绘制到目标上下文中的,源图像的矩形选择框的宽度
 * @param  {number} height 需要绘制到目标上下文中的,源图像的矩形选择框的高度
 * @param  {number} dWidth 在目标画布上绘制图像的宽度。 允许对绘制的图像进行缩放
 * @param  {number} dHeight 在目标画布上绘制图像的高度。 允许对绘制的图像进行缩放
 */
function drawImage(ctx, imgPath, x, y, width, height, dWidth, dHeight) {
  if (!ctx || !imgPath) {
    return;
  }

  if (ctx) {
    ctx.drawImage(imgPath, 0, 0, width, height, x, y, dWidth, dHeight);
  }
}

/* ---------------end DOM的相关方法------------------------*/

/**
 * 做初始化工作, 获取所有图片的的信息, 包括宽和高.
 * @param  {array} images 所有小图的地址的数组
 * @param  {function} 所有图片的宽高都获取成功后的回调.
 */
function init(images, cb) {
  const getNameFromUrl = function(url) {
    const arr = url.split('/');

    return arr[arr.length - 1];
  };

  if (!images || !images.length) {
    return;
  }

  const allPromises = [];
  images.forEach(url => {
    allPromises.push(loadImage(url));
  });

  if (allPromises && allPromises.length) {
    Promise.all(allPromises).then(imagesArray => {
      cb && cb(imagesArray);
    });
  }
}

/**
 * 根据9张小图片, 拼接成一张指定宽高的大图.
 * @param  {array} images 所有小图的地址的数组
 * @param  {number} targetWidth 拼接后的图片的宽
 * @param  {number} targetHeight 拼接后的图片的高
 * @param  {object} paddings 图片白边:包括top, right, bottom, left四个方向的值.
 * @param  {function} 拼接完成后的回调, 参数里包含了拼接后的大图.
 */
export const combine = (
  ctx,
  images,
  targetWidth,
  targetHeight,
  paddings,
  outPaddings,
  cb
) => {
  targetWidth = parseInt(targetWidth) || 0;
  targetHeight = parseInt(targetHeight) || 0;

  paddings.top = parseInt(paddings.top) || 0;
  paddings.right = parseInt(paddings.right) || 0;
  paddings.bottom = parseInt(paddings.bottom) || 0;
  paddings.left = parseInt(paddings.left) || 0;

  const oPaddings = outPaddings || {
    top: 0,
    left: 0,
    right: 0,
    bottom: 0
  };


  // 参数检查.
  if (!images || images.length !== 9 || !targetWidth || !targetHeight) {
    return;
  }

  // 获取所有图片的信息, 包括宽和高.
  init(images, allImages => {
    // 计算每一张小图在canvas绘制时的坐标和宽高
    const imagesOptionsInCanvas = computedImageParamsInCanvas(
      allImages,
      targetWidth,
      targetHeight,
      paddings,
      oPaddings
    );

    // 新建一个新的canvas作为绘图容器
    const { container } = imagesOptionsInCanvas;

    // 把图片绘制到canvas上.
    imagesOptionsInCanvas.images.forEach(imgOption => {
      drawImage(
        ctx,
        imgOption.img.path,
        imgOption.x,
        imgOption.y,
        imgOption.img.width,
        imgOption.img.height,
        imgOption.dWidth,
        imgOption.dHeight
      );
    });

    ctx.draw(false, cb);
  });
};

在小程序中的使用方式:

test.wxml:

<canvas canvas-id="combine" class="hidden-canvas" disable-scroll="true" style="width:{{width}}px;height:{{height}}px;"></canvas>

test.js:

import { combine } from './point9scale';
const { screenWidth } = wx.getSystemInfoSync();
const pixelRatio = screenWidth / 750;

const pieceImages = []; // 这里是9张素材图

Component({
  data: {
    width: 300 * pixelRatio,
    height: 300 * pixelRatio
  },

  ready() {
    const { width, height } = this.data;
    const combineCtx = wx.createCanvasContext('combine', this);

    combine(combineCtx, pieceImages, width, height, {}, null, () => {
      wx.canvasToTempFilePath(
       {
         x: 0,
         y: 0,
         width,
         height,
         canvasId: 'combine',
         success(res) {
           // 这里你可以通过triggerEvent的方式将临时路径传递给父页面或者组件
           callback && callback(res.tempFilePath);
         },
         fail(err) {}
       },
       this
     );
    });
  }
});

test.wxss:

.hidden-canvas {
   position: absolute;
   left: -9999px;
 }

  • 支付宝二维码 支付宝
  • 微信二维码 微信
相关文章