闲得无聊来给图片加个马赛克吧(笑

902 阅读1分钟

第一步当然是获取图片咯

<div class="group">
  <p>原图:</p>
  <canvas id="out1"></canvas>
</div>
<div class="group">
  <p>马赛克:</p>
  <canvas id="out2"></canvas>
</div>
const getImgData = (url) => {
  let dom = document.createElement('canvas');
  const ctx = dom.getContext('2d');
  const image = new Image();
  image.src = url;
  return new Promise(resolve => {
    image.onload = () => {
      const width = image.width;
      const height = image.height;
      dom.width = width;
      dom.height = height;
      console.log(`图片宽高:${width}, ${height};像素点:${width * height}`);
      ctx.drawImage(image, 0, 0, width, height);
      // 获取图片信息
      const imgData = ctx.getImageData(0, 0, width, height);
      dom = null;
      resolve({
        data: imgData.data,
        width,
        height,
      });
    }
  })
}

很好理解吧~记得在服务下打开html哦 这里推荐http-server 好用的一批。

ctx.getImageData是获取图片数据的接口(有跨域问题),返回数组:[r1,g1,b1,a1,r2,g2,b2,a2...],其中是从左至右、从上至下的像素的rgba一维数组。

随便塞一张图执行一下吧~

getImgData('./3.jpg').then(({
  data,
  width,
  height,
}) => {
    consolle.log(data, width, height);
})

getImgData里得出结果:图片宽高:225, 224;像素点:50400

then里面得出结果:Unit8ClampedArray(201600)[255,255,255,255,255,255,...] 225 224

第二步就是打印图片

我们要弄个马赛克,当然是得把一维数组做成矩阵,先把这一步做了吧~

打个比方,这是一个4个像素的2x2的方块

[255,0,0,255,0,255,0,255,0,0,255,255,0,0,0,255]

我们要弄成一个矩阵

[
    [
        [255,0,0,255],[0,255,0,255]
    ],
    [
        [0,0,255,255],[0,0,0,255]
    ]
]

不多说,上代码

/**
 * 图片数组+宽+高 => 矩阵数组
 * @param {Array<number>} arr 图片数组
 * @param {number} width 宽
 * @param {number} height 高
 */
const splitImgData = (arr, width, height) => {
  const list = [];
  const result = [];
  const len = Math.ceil(arr.length / 4);
  // 行数获取
  for (let i = 0; i < len; i++) {
    const start = i * 4;
    list.push([arr[start], arr[start + 1], arr[start + 2], arr[start + 3]]);
  }
  // 纵列嵌套
  for (let h = 0; h < height; h++) {
    const temp = [];
    for (let w = 0; w < width; w++) {
      temp.push(list[h * width + w]);
    }
    result.push(temp);
  }
  return result
}

然后就是打印图片代码

/**
 * 矩阵数组=>图片
 * @param {Array<Array<Array<number>>>} matrix 
 * @param {Document} dom canvas DOM
 */
const printImg = (matrix, dom) => {
  const ctx = dom.getContext('2d');
  const width = matrix[0].length;
  const height = matrix.length;
  dom.width = width;
  dom.height = height;
  // 创建图片数组对象
  const matrixObj = ctx.createImageData(width, height);
  // 矩阵解耦,空数组设为白色
  const _matrix = matrix.flat(1).map(arr => arr.length ? arr : [0,0,0,0]).flat(1);
  // 图片数组 加入图片数组对象
  matrixObj.data.set(_matrix)
  // 绘制
  ctx.putImageData(matrixObj, 0, 0);
}

createImageData接受一维数组,所以要用flat转换。

这里flat解释一下吧,是个ES6的新方法,用来数组的降维打击(笑),传1则是向下降维一层,传Infinity则是降维到一维,一向箔了属于是。

执行一下吧 不贴图了

const canvas_out1 = document.getElementById('out1');

getImgData('./3.jpg').then(({
  data,
  width,
  height,
}) => {
    const matrix = splitImgData(data, width, height);
    printImg(matrix, canvas_out1);
})

马赛克!

咋搞马赛克呢,先设定x y width height,然后加入granular(颗粒度)概念,每一个granular*granular的方块内部颜色替换为rgba四项值的平均。

1、第一步解决切割granular*granular方块

/**
 * 获取 x y 那个点的颜色数组
 * @param {Array<Array<Array<number>>>} matrix 
 * @param {number} y 
 * @param {number} x 
 */
const getMatrix_XY = (matrix, y, x) => {
  return matrix[y] ? matrix[y][x] || [] : []
}

/**
 * 打码(矩阵数组=>矩阵数组)
 * @param {Array<Array<Array<number>>>} matrix 
 * @param {number} x 
 * @param {number} y 
 * @param {number} width 
 * @param {number} height 
 * @param {number} granular 颗粒度 像素
 */
const imgCoding = (matrix, x, y, width, height, granular) => {
  const _matrix = JSON.parse(JSON.stringify(matrix));
  /* 设定一个对象存颗粒方块
  * {
  *    'g_x, g_y': {
  *      'x,y': [r,g,b,a],
  *      'x,y': [r,g,b,a],
  *    },
  *    'g_x, g_y': {
  *      'x,y': [r,g,b,a],
  *      'x,y': [r,g,b,a],
  *    }
  * }
  */
  const codingBlock = {};
  for (let w = 0; w < width; w++) {
    const groupX = Math.ceil((w + 1) / granular) - 1
    for (let h = 0; h < height; h++) {
      const groupY = Math.ceil((h + 1) / granular) - 1
      const target = getMatrix_XY(_matrix, w + x, h + y);
      if (target.length) {
        if (codingBlock[`${groupX},${groupY}`]) {
          codingBlock[`${groupX},${groupY}`][`${w + x},${h + y}`] = target
        } else {
          codingBlock[`${groupX},${groupY}`] = {}
          codingBlock[`${groupX},${groupY}`][`${w + x},${h + y}`] = target
        }
      }
    }
  }
}

其中'g_x、g_y'只是用来定义颗粒块的下标,'x,y'用来定位,codingBlock就储存了每个颗粒块内每个像素的颜色,后面就是取平均数了。

/**
 * 获取平均数(返回整数)
 * @param {Array<number>} arr
 */
const getAverage = arr => Math.ceil(eval(arr.join('+')) / arr.length);

/**
 * 打码(矩阵数组=>矩阵数组)
 * @param {Array<Array<Array<number>>>} matrix 
 * @param {number} x 
 * @param {number} y 
 * @param {number} width 
 * @param {number} height 
 * @param {number} granular 颗粒度像素
 */
const imgCoding = (matrix, x, y, width, height, granular) => {
  const _matrix = JSON.parse(JSON.stringify(matrix));
  /* 打码块
   * {
   *    'g_x, g_y': {
   *      'x,y': [r,g,b,a],
   *      'x,y': [r,g,b,a],
   *    },
   *    'g_x, g_y': {
   *      'x,y': [r,g,b,a],
   *      'x,y': [r,g,b,a],
   *    }
   * }
   */
  const codingBlock = {};
  for (let w = 0; w < width; w++) {
    const groupX = Math.ceil((w + 1) / granular) - 1
    for (let h = 0; h < height; h++) {
      const groupY = Math.ceil((h + 1) / granular) - 1
      const target = getMatrix_XY(_matrix, w + x, h + y);
      if (target.length) {
        if (codingBlock[`${groupX},${groupY}`]) {
          codingBlock[`${groupX},${groupY}`][`${w + x},${h + y}`] = target
        } else {
          codingBlock[`${groupX},${groupY}`] = {}
          codingBlock[`${groupX},${groupY}`][`${w + x},${h + y}`] = target
        }
      }
    }
  }
  for (const k in codingBlock) {
    const arr_r = [];
    const arr_g = [];
    const arr_b = [];
    const arr_a = [];
    for (const key in codingBlock[k]) {
      arr_r.push(codingBlock[k][key][0]);
      arr_g.push(codingBlock[k][key][1]);
      arr_b.push(codingBlock[k][key][2]);
      arr_a.push(codingBlock[k][key][3]);
    }
    const color = [
      getAverage(arr_r),
      getAverage(arr_g),
      getAverage(arr_b),
      getAverage(arr_a),
    ];
    for (const key in codingBlock[k]) {
      const [_x, _y] = key.split(',');
      _matrix[Number(_x)][Number(_y)] = color
    }
  }
  return _matrix
}

以10为颗粒度执行!

const canvas_out2 = document.getElementById('out2');

getImgData('./3.jpg').then(({
  data,
  width,
  height,
}) => {
  const matrix = splitImgData(data, width, height);
  printImg(imgCoding(matrix, 0, 0, 160, 160, 10), canvas_out2)
})

得到返回图片:

好家伙 这码厚的!

结束啦 附赠个图片旋转代码,totate(Math.PI/2)出来挨打

/**
 * 旋转
 * @param {Array<Array<Array<number>>>} matrix 
 * @param {'left' | 'right'} direction
 */
const rotate = (matrix, direction) => {
  const _matrix = JSON.parse(JSON.stringify(matrix));
  const new_width = matrix[0].length;
  const new_height = matrix.length;
  const newMatrix = [];
  // 左转
  if (direction === 'left') {
    for (let x = 0; x < new_width; x++) {
      const temp = [];
      for (let y = 0; y < new_height; y++) {
        temp.push(getMatrix_XY(_matrix, y, new_width - 1 - x))
      }
      newMatrix.push(temp);
    }
  }
  // 右转
  if (direction === 'right') {
    for (let x = 0; x < new_width; x++) {
      const temp = [];
      for (let y = 0; y < new_height; y++) {
        temp.push(getMatrix_XY(_matrix, new_height - 1 - y, x))
      }
      newMatrix.push(temp);
    }
  }
  return newMatrix;
}

下班无聊 代码无聊 各种无聊