前端 Canvas 画像素画

946 阅读3分钟

前端 Canvas 像素画

前言

最近看像素画的功能挺火热的。查阅了相关资料发现实现也挺简单的。所以想着自己来写一个图片转像素画的功能。我们先来看看效果。

WechatIMG325.png

image.png

总体流程图

image.png

代码实现

本地图片转 base64

upload.tsx

import React, { useState, createRef } from 'react';

interface Iprops extends React.PropsWithChildren<{}>{
  handleChange: (base64: string) => void;
}
const UploadPicture = (props: Iprops) => {
  const { handleChange, children } = props;
  const inputRef = createRef<HTMLInputElement>();
  const [showInput, setShowInput] = useState(true);
  const imgTypeList = ['image/jpeg', 'image/jpg', 'image/png'];

  const selectImage = () => {
    inputRef.current?.click();
  };

  const onChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const { files } = e.target;
    if (!files || !files.length) return;
    const file = files[0];
    if (imgTypeList.indexOf(file.type) === -1) {
      return;
    }
    const imageBase64 = await getImageBase64(file);
    cleanInputSelectFile();
    handleChange(imageBase64);
  };

  const getImageBase64 = async (file: File): Promise<string> => {
    const base64Url = await readFile(file);
    return base64Url;
  };

  const readFile = async (file: File): Promise<string> =>
    new Promise(resolve => {
      const fileReader = new FileReader();
      fileReader.onload = e => {
        resolve((e?.target?.result || '') as string);
      };
      fileReader.readAsDataURL(file);
    });

  const cleanInputSelectFile = () => {
    setShowInput(false);
    const timer = setTimeout(() => {
      setShowInput(true);
      clearTimeout(timer);
    }, 0);
  };

  return (
    <div onClick={selectImage}>
      {showInput && <input style={{ height: '0px', width: '0px' }} ref={inputRef} type="file" onChange={onChange} />}
      {children}
    </div>
  );
};

export default UploadPicture;

这是一个通用化的组件,不想写这么繁琐可以直接使用 input 框 + onChange 的方法

pixel.ts(对图片 base64 处理的函数)

interface PixelPictureData {
  // canvas 图片数据,用于绘制 canvas
  imageData: ImageData;
  // rgba 颜色数据
  rgbaData: number[][][];
  // 16 进制颜色数据,前面不带 #
  hexifyData: string[][],
  // 图片高度
  height: number;
  // 图片宽度
  width: number;
  // 图片 base64 数据
  base64: string;
}

/**
 * @description: canvas 的 ImageData 转成 base64
 * @param {object} data
 * @return {*}
 */
const transformImageData2Base64 = (data: { imageData: ImageData; height: number; width: number }): string => {
  const { imageData, height, width } = data;
  const canvas = document.createElement('canvas');
  canvas.height = height;
  canvas.width = width;
  const ctx = canvas.getContext('2d');
  ctx?.putImageData(imageData, 0, 0);
  return canvas.toDataURL('image/jpeg', 1);
};

/**
 * @description: 获取一块 pixel 的颜色
 * @param {object} data
 * @return {*}
 */
const getPixelColor = (data: { img?: ImageData; x: number; y: number }): number[] | undefined => {
  const { img, x, y } = data;
  if (!img) {
    return;
  }
  const pixelColor = [];
  const imgData = img.data;
  const imgWidth = img.width;

  // 获取颜色偏移量位置:column(y)*width(w)+row(x),当偏移量大于图片长度时获取的为 undefined,需要去除该像素信息
  // r
  pixelColor[0] = imgData[(y * imgWidth + x) * 4];
  // g
  pixelColor[1] = imgData[(y * imgWidth + x) * 4 + 1];
  // b
  pixelColor[2] = imgData[(y * imgWidth + x) * 4 + 2];
  // a
  pixelColor[3] = imgData[(y * imgWidth + x) * 4 + 3];
  // 判读像素颜色信息是否获取到
  return pixelColor[3] === undefined ? undefined : pixelColor;
};

/**
 * @description: 设置一块 pixel 的颜色
 * @param {object} data
 * @return {*}
 */
const setPixelColor = (data: { img?: ImageData; x: number; y: number; pixelColor: number[] }) => {
  const { img, x, y, pixelColor } = data;
  if (!img) {
    return;
  }
  const imgData = img.data;
  const imgWidth = img.width;
  const [r, g, b, a] = pixelColor;
  imgData[(y * imgWidth + x) * 4] = r;
  imgData[(y * imgWidth + x) * 4 + 1] = g;
  imgData[(y * imgWidth + x) * 4 + 2] = b;
  imgData[(y * imgWidth + x) * 4 + 3] = a;
};

/**
 * @description: 获取像素画相关信息
 * @param {*}
 * @return {*}
 */
export const getPixelPictureData = async (data: {
  // 图片 base64 数据
  base64: string;
  // 长边对应 pixel 块数
  size: number;
  // 每块 pixel 的宽高,单位:像素
  pixelBlockSize: number;
  // 像素块之间是否设置间隔
  addBorder?: boolean;
  // border 颜色,rgba
  borderColor?: number[];
}): Promise<PixelPictureData | undefined> =>
  new Promise(resolve => {
    const whiteRgba = [255, 255, 255, 255];
    const defaultData = {
      // 默认长边 pixel 块数
      size: 24,
      // 默认 pixel 宽高,单位:像素
      pixelBlockSize: 20,
      // 默认 pixel 之间不增加 border
      addBorder: false,
    };
    const { base64, size, pixelBlockSize, addBorder, borderColor } = { ...defaultData, ...data };
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const image = new Image();
    image.src = base64;
    image.onload = () => {
      const { height, width } = image;
      // 每一块 pixel 的宽高
      const pixelSize = (height > width ? height : width) / size;
      canvas.width = width;
      canvas.height = height;
      ctx?.drawImage(image, 0, 0, width, height);
      const imageData = ctx?.getImageData(0, 0, width, height);

      // 获取像素画每个像素的 rgba 颜色
      const rgbaData: number[][][] = [];
      for (let i = 0; i < image.width / pixelSize; i++) {
        rgbaData.push([]);
        for (let j = 0; j < image.height / pixelSize; j++) {
          // 获取的颜色为中间点的颜色
          const pixelColor = getPixelColor({
            img: imageData,
            x: Math.floor(i * pixelSize + pixelSize / 2),
            y: Math.floor(j * pixelSize + pixelSize / 2),
          });
          if (!pixelColor) {
            break;
          }
          rgbaData[i][j] = pixelColor;
        }
      }

      // 图片宽高转换为像素画图片的宽高
      let pixelPicWidth = 0;
      let pixelPciHeight = 0;
      if (height > width) {
        pixelPicWidth = Math.floor(width / pixelSize) * pixelBlockSize;
        pixelPciHeight = pixelBlockSize * size;
      } else {
        pixelPicWidth = pixelBlockSize * size;
        pixelPciHeight = Math.floor(height / pixelSize) * pixelBlockSize;
      }

      // 设置 piexl 图片的 ImageData
      const newImageData = ctx?.createImageData(pixelPicWidth, pixelPciHeight);
      rgbaData.forEach((row, rowIndex) => {
        row?.forEach((pixelColor, columnIndex) => {
          for (let pixelX = 0; pixelX < pixelBlockSize; pixelX++) {
            for (let pixelY = 0; pixelY < pixelBlockSize; pixelY++) {
              let curBorderColor;
              if (addBorder) {
                // 最后一行和一列不绘制 border
                if (
                  (pixelX === pixelBlockSize - 1 && rowIndex !== rgbaData.length - 1)
                  || (pixelY === pixelBlockSize - 1 && columnIndex !== row.length - 1)
                ) {
                  curBorderColor = borderColor || whiteRgba;
                }
              }
              // 轮循每一个像素点:pixelX,加入随机点i*pixelSize+pixelX
              setPixelColor({
                img: newImageData,
                x: rowIndex * pixelBlockSize + pixelX,
                y: columnIndex * pixelBlockSize + pixelY,
                pixelColor: curBorderColor || pixelColor,
              });
            }
          }
        });
      });

      // 16 进制颜色数据
      let hexifyData: string[][] = [];
      hexifyData = rgbaData?.map(item =>
        item.map(item => {
          const [r, g, b] = item;
          return `0${r.toString(16)}`.slice(-2) + `0${g.toString(16)}`.slice(-2) + `0${b.toString(16)}`.slice(-2);
        })
      );

      if (newImageData) {
        const height = pixelPciHeight;
        const width = pixelPicWidth;
        const base64 = transformImageData2Base64({ imageData: newImageData, height, width });
        resolve({ imageData: newImageData, rgbaData, hexifyData, height, width, base64 });
      }
    };
  });

把处理得到的 base64 放入 image 标签就可以看到像素画以后的图片了

参考资料

blog.csdn.net/weixin_3705…