Canvas滤镜

544 阅读5分钟

什么是 Canvas?

HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。

画布是一个矩形区域,您可以控制其每一像素。

canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。

Canvas图片操作

canvas图像操作主要以下api:drawImage,createImageData, getImageData, putImageData ;

方法

描述

drawImage

在画布上绘制图像、画布或视频

createImageData

创建新的、空白的 ImageData 对象

getImageData

返回 ImageData 对象,该对象为画布上指定的矩形复制像素数据

putImageData

把图像数据(从指定的 ImageData 对象)放回画布上

在以上基础上,我们可以在canvas元素中绘制我们的图片, 假设我们的图片为image.jpg

const img = new Image() // 声明新的Image对象
img.src = "./img/photo.jpg"
const canvas = document.querySelector("#myCanvas"); // 获取canvas元素
const ctx = canvas.getContext("2d"); 

img.onload = function() {
   // 根据image大小,指定canvas大小
   canvas.width = img.width;
   canvas.height = img.height;
   // 将图片绘制到canvas中
   ctx.drawImage(img, 0, 0, canvas.width,canvas.height);
}

这样我们就将图片绘制到canvas中了

canvas滤镜

这里我们主要是对imageData进行操作

getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。

对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:

R - 红色 (0-255)

G - 绿色 (0-255)

B - 蓝色 (0-255)

A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)

color/alpha 以数组形式存在,并存储于 ImageData 对象的 data 属性中。

接下来让我们先搭建一个基本项目框架,这里我使用的是create-react-app生成的项目

import React, { useState, useLayoutEffect, useRef } from 'react';

function Home() {
const [size, setSize] = useState({ width: 0, height: 0 });
  const canvasRef = useRef(null);
  const imgRef = useRef(null);

  const imageLoad = () => {
    const width = imgRef.current.width;
    const height = imgRef.current.height;
    setSize({ width, height });
    canvasRef.current.width = width;
    canvasRef.current.height = height;
    queryImageData();
  };

  useLayoutEffect(() => {
    const { width, height } = size;
    const ctx = canvasRef.current.getContext('2d');
     //图片加载完成后 将图片绘制到canvas中
    ctx.drawImage(imgRef.current, 0, 0, imgRef.current.naturalWidth, imgRef.current.naturalHeight, 0, 0, width, height);
  }, [size]);

return (
    <div className='home-view'>
      <div className='home-content'>
        <div className='content'>
          <img onLoad={imageLoad} ref={imgRef} src={img} alt='little' />
        </div>
        <div className='content'>
          <canvas ref={canvasRef} id='canvas' />
        </div>
      </div>
    </div>
  );
}

// css我就不贴了 基本没内容

当图片加载完成后, 我们将图片放入canvas中,这里我们未对图片做任何处理;

接下来就是对图片添加滤镜

为图片添加滤镜就是对imageData中data进行操作,然后将改变后的数据再次放入到canvas中展现

接下来我们添加操作按钮在页面底部

import React, { useState, useLayoutEffect, useRef } from 'react';
import CanvasRect from '../../utils/draw';
import photoFilter from '../../utils/filter';

import img from '../../static/img/4a5e48e736d12f2ef27d70b84fc2d56284356824.jpg';

import './index.less';

const buttonList = [
  { text: '正常', code: 'normal' },
  { text: '灰度滤镜', code: 'hdlj' },
  { text: '黑白滤镜', code: 'hblj' },
  { text: '反向滤镜', code: 'fxlj' },
  { text: '去色滤镜', code: 'qslj' },
  { text: '单色滤镜', code: 'dslj' },
  { text: '怀旧滤镜', code: 'hjlj' },
  { text: '熔铸滤镜', code: 'rzlj' },
  { text: '冰冻滤镜', code: 'bdlj' },
  { text: '连环画滤镜', code: 'lhhlj' },
  { text: '暗调滤镜', code: 'adlj' },
  { text: '高斯模糊滤镜', code: 'gsmmlj' },
];

function Home() {
  const [size, setSize] = useState({ width: 0, height: 0 });
  const canvasRef = useRef(null);
  const imgRef = useRef(null);
  const [type, setType] = useState(-1);

  const imageLoad = () => {
    const width = imgRef.current.width;
    const height = imgRef.current.height;
    setSize({ width, height });
    canvasRef.current.width = width;
    canvasRef.current.height = height;
    queryImageData();
  };

  useLayoutEffect(() => {
    const { width, height } = size;
    const ctx = canvasRef.current.getContext('2d');
    ctx.drawImage(imgRef.current, 0, 0, imgRef.current.naturalWidth, imgRef.current.naturalHeight, 0, 0, width, height);
  }, [size]);

  const frameCanvas = () => {
    const img = imgRef.current;
    const width = img.width;
    const height = img.height;
    const naturalWidth = img.naturalWidth;
    const naturalHeight = img.naturalHeight;
    let canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    let context = canvas.getContext('2d');
    return [canvas, context, width, height, naturalWidth, naturalHeight];
  };

  const queryImageData = () => {
    const img = imgRef.current;
    const [, context, width, height, naturalWidth, naturalHeight] = frameCanvas();
    context.drawImage(img, 0, 0, naturalWidth, naturalHeight, 0, 0, width, height);
    const imgData = context.getImageData(0, 0, width, height);
    return imgData;
  };

  const change = async (type) => {
    const { width, height } = size;
    await setType(type);
    const func = photoFilter[type];
    const baseImageData = queryImageData(); // 获取原始图片imageData
    const imageData = func ? func(baseImageData) : baseImageData; // 通过滤镜改变图片
    const img = filterImage(imageData); // 得到新图片
    const ctx = canvasRef.current.getContext('2d');
    new CanvasRect(img, ctx); // 将图片放入至canvas
    ctx.clearRect(0, 0, width, height);
  };

  const filterImage = (imageData) => {
    const [canvas, context] = frameCanvas();
    context.putImageData(imageData, 0, 0);
    const src = canvas.toDataURL();
    return src;
  };

  return (
    <div className='home-view'>
      <div className='home-content'>
        <div className='content'>
          <img onLoad={imageLoad} ref={imgRef} src={img} alt='little' />
        </div>
        <div className='content'>
          <canvas ref={canvasRef} id='canvas' />
        </div>
      </div>
      <div className='home-footer'>
        {buttonList.map((v) => {
          return (
            <div key={v.code} onClick={() => change(v.code)} className={`footer-button ${v.code === type ? 'active' : ''}`}>
              {v.text}
            </div>
          );
        })}
      </div>
    </div>
  );
}

photoFilter中就是滤镜的一些算法,CanvasRect是将图片放入到canvas中

灰度滤镜

对每个像素的R, G, B数值取平均值

function (imgData) {
    let data = imgData.data;
    for (var i = 0; i < data.length; i += 4) {
      var grey = (data[i] + data[i + 1] + data[i + 2]) / 3;
      data[i] = data[i + 1] = data[i + 2] = grey;
    }
    return imgData;
  }

黑白滤镜

对每个像素的R, G, B 三个值平均值大于125则设为255 反之设为0

function (imgData) {
    let data = imgData.data;
    for (var i = 0; i < data.length; i += 4) {
      var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
      data[i] = data[i + 1] = data[i + 2] = avg >= 125 ? 255 : 0;
    }
    return imgData;
  }

反向滤镜

对每个像素的R, G, B 取反

function (imgData) {
    let data = imgData.data;
    for (var i = 0; i < data.length; i += 4) {
      data[i] = 255 - data[i];
      data[i + 1] = 255 - data[i + 1];
      data[i + 2] = 255 - data[i + 2];
    }
    return imgData;
  }

去色滤镜

将R, G, A 同时设置为三个值中最大和最小和的二分之一

function (imgData) {
    let data = imgData.data;
    for (let i = 0; i < data.length; i++) {
      let avg = Math.floor((Math.min(data[i], data[i + 1], data[i + 2]) + Math.max(data[i], data[i + 1], data[i + 2])) / 2);
      data[i] = data[i + 1] = data[i + 2] = avg;
    }
    return imgData;
  }

单色滤镜

保留R, G, B中任意一个数值 将其他两个设置为0

function (imgData) {
    let data = imgData.data;
    for (let i = 0; i < data.length; i++) {
      data[i * 4 + 2] = 0;
      data[i * 4 + 1] = 0;
    }
    return imgData;
  }

怀旧滤镜

function (imgData) {
    let data = imgData.data;
    for (let i = 0; i < data.length; i++) {
      let r = data[i * 4],
        g = data[i * 4 + 1],
        b = data[i * 4 + 2];
      let newR = 0.393 * r + 0.769 * g + 0.189 * b;
      let newG = 0.349 * r + 0.686 * g + 0.168 * b;
      let newB = 0.272 * r + 0.534 * g + 0.131 * b;
      let rgbArr = [newR, newG, newB].map((e) => {
        return e < 0 ? 0 : e > 255 ? 255 : e;
      });
      [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]] = rgbArr;
    }
    return imgData;
  }

熔铸滤镜

function (imgData) {
    let data = imgData.data;
    for (let i = 0; i < data.length; i++) {
      let r = data[i * 4],
        g = data[i * 4 + 1],
        b = data[i * 4 + 2];
      let newR = (r * 128) / (g + b + 1);
      let newG = (g * 128) / (r + b + 1);
      let newB = (b * 128) / (g + r + 1);
      let rgbArr = [newR, newG, newB].map((e) => {
         return e < 0 ? e * -1 : e;
      });
      [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]] = rgbArr;
    }
    return imgData;
  }

冰冻滤镜

function (imgData) {
    let data = imgData.data;
    for (let i = 0; i < data.length; i++) {
      let r = data[i * 4],
        g = data[i * 4 + 1],
        b = data[i * 4 + 2];
      let newR = ((r - g - b) * 3) / 2;
      let newG = ((g - r - b) * 3) / 2;
      let newB = ((b - g - r) * 3) / 2;
      let rgbArr = [newR, newG, newB].map((e) => {
        return e < 0 ? e * -1 : e;
      });
      [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]] = rgbArr;
    }
    return imgData;
  }

连环画滤镜

 function (imgData) {
    let data = imgData.data;
    for (let i = 0; i < data.length; i++) {
      let r = data[i * 4],
        g = data[i * 4 + 1],
        b = data[i * 4 + 2];
      let newR = (Math.abs(g - b + g + r) * r) / 256;
      let newG = (Math.abs(b - g + b + r) * r) / 256;
      let newB = (Math.abs(b - g + b + r) * g) / 256;
      let rgbArr = [newR, newG, newB].map((e) => {
        return e < 0 ? 0 : e > 255 ? 255 : e;
      });
      [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]] = rgbArr;
    }
    return imgData;
  }

暗调滤镜

function (imgData) {
    let data = imgData.data;
    for (let i = 0; i < data.length; i++) {
      let r = data[i * 4],
        g = data[i * 4 + 1],
        b = data[i * 4 + 2];
      let newR = (r * r) / 255;
      let newG = (g * g) / 255;
      let newB = (b * b) / 255;
      let rgbArr = [newR, newG, newB].map((e) => {
        return e < 0 ? 0 : e > 255 ? 255 : e;
      });
      [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]] = rgbArr;
    }
    return imgData;
  }

高斯模糊滤镜

function (imgData, radius = 5, sigma = radius / 3) {
    let handleEdge = (i, x, w) => {
      var m = x + i;
      if (m < 0) {
        m = -m;
      } else if (m >= w) {
        m = w + i - x;
      }
      return m;
    };

    var pixes = imgData.data,
      height = imgData.height,
      width = imgData.width;
    var gaussEdge = radius * 2 + 1;
    var gaussMatrix = [],
      gaussSum = 0,
      a = 1 / (2 * sigma * sigma * Math.PI),
      b = -a * Math.PI;

    for (var i = -radius; i <= radius; i++) {
      for (var j = -radius; j <= radius; j++) {
        var gxy = a * Math.exp((i * i + j * j) * b);
        gaussMatrix.push(gxy);
        gaussSum += gxy;
      }
    }
    var gaussNum = (radius + 1) * (radius + 1);
    for (let i = 0; i < gaussNum; i++) {
      gaussMatrix[i] /= gaussSum;
    }

    for (let x = 0; x < width; x++) {
      for (let y = 0; y < height; y++) {
        let r = 0,
          g = 0,
          b = 0;
        for (let i = -radius; i <= radius; i++) {
          let m = handleEdge(i, x, width);
          for (let j = -radius; j <= radius; j++) {
            let mm = handleEdge(j, y, height);
            let currentPixId = (mm * width + m) * 4;
            let jj = j + radius;
            let ii = i + radius;
            r += pixes[currentPixId] * gaussMatrix[jj * gaussEdge + ii];
            g += pixes[currentPixId + 1] * gaussMatrix[jj * gaussEdge + ii];
            b += pixes[currentPixId + 2] * gaussMatrix[jj * gaussEdge + ii];
          }
        }
        let pixId = (y * width + x) * 4;
        pixes[pixId] = ~~r;
        pixes[pixId + 1] = ~~g;
        pixes[pixId + 2] = ~~b;
      }
    }
    return imgData;
  }

。。。 暂时就这些吧,以后再补充一下, 第一次发文,没过多的文字,希望大家不要介意,希望大家可以学到一点点好玩的东西。

参考链接

图像滤镜特效(反色、浮雕、雕刻、怀旧、冰冻、暗调)(一)

图像滤镜特效(曝光、霓虹、连环画、熔铸)(二)

代码地址

canvas-filter

webpack loader 小小的滤镜 没啥用

❤️ 感谢大家