实践[前端] 通过canvas替换背景色

102 阅读3分钟

由于我使用的操作系统无法安装ps,但是我也不想使用在线的替换工具替换,怕个人信息泄露,于是就想着能不能自己写一个工具出来实现相同的效果,最终我选择使用canvas的方式实现该效果

image.png

原理是通过img读取图片,并写入canvas,然后通过canvasContext提供的函数getImageData读取像素的rgba色值,通过对rgb色值进行区间判断,并设置成指定颜色,就可以实现简单的换背景效果了。

  • 存在问题:边缘模糊色值无法分离,这个得后期研究一下

源码讲解

首先我们准备获取图片数据,并获得宽高

  let imgWidth=0,imgHeight=0,imgPix=0;
  const image = new Image()
  image.src = IMG_URL
  const {promise,resolve} = withResolvers<void>()
  image.onload=()=>{
    resolve?.(undefined)
  }
  await promise;
  imgWidth = image.width
  imgHeight=image.height

然后我们获取canva和canvascontext,并设置canvas宽度高度和context宽度和高度

  const canvas = document.querySelector("#canvas") as HTMLCanvasElement
  const context = canvas.getContext('2d') as CanvasRenderingContext2D
  
  canvas.width = imgWidth
  canvas.height = image.height
  canvas.style.width = imgWidth / 10 + 'px'
  canvas.style.height = imgHeight / 10 + 'px'

最后我们将图片写入canvas,并读取数据对比修改,和重新写入,实现图片改变背景色

  context.drawImage(image, 0, 0, imgWidth, imgHeight)
  const imgData = context.getImageData(0, 0, imgWidth, imgHeight)
  const data = imgData.data
  const step = 130; // 取值区间,可动态调整
  for(let i = 0; i< data.length;i += 4) {
    if(
      filterColor[0]+50>data[i+0]&&filterColor[0]-50 <data[i+0]&&
      filterColor[1]+step>data[i+1]&&filterColor[1]-step<data[i+1]&&
      filterColor[2]+step>data[i+2]&&filterColor[2]-step<data[i+2]
    ) {
      data[i+0] = 255
      data[i+1] = 255
      data[i+2] = 255
    }
  }
  context.putImageData(imgData, 0, 0)

所使用的工具函数

  1. 串行promise
function withResolvers<T>(){
  interface Obj{
    promise?:Promise<T>,
    resolve?:(value: T | PromiseLike<T>) => void
    reject?:(value: T | PromiseLike<T>) => void
  }
  const obj:Obj = {}
  obj.promise = new Promise<T>((res,rej)=>{
    obj.resolve=res;
    obj.reject=rej
  })
  return obj;
}
  1. 6位16进制色值转数值数组
function hexToNum(s:string){
  let str = s
  const colorArr:number[] = []
  str = str.replace("#",'')
  const strArr = str.split("")
  if(strArr.length === 6) {
    colorArr[0] = parseInt('0x' + strArr[0]+strArr[1])
    colorArr[1] = parseInt('0x' + strArr[2]+strArr[3])
    colorArr[2] = parseInt('0x' + strArr[4]+strArr[5])
  }
  return colorArr
}

完整代码

const IMG_URL = '/img/img.jpg'
function withResolvers<T>(){
  interface Obj{
    promise?:Promise<T>,
    resolve?:(value: T | PromiseLike<T>) => void
    reject?:(value: T | PromiseLike<T>) => void
  }
  const obj:Obj = {}
  obj.promise = new Promise<T>((res,rej)=>{
    obj.resolve=res;
    obj.reject=rej
  })
  return obj;
}
function hexToNum(s:string){
  let str = s
  const colorArr:number[] = []
  str = str.replace("#",'')
  const strArr = str.split("")
  if(strArr.length === 6) {
    colorArr[0] = parseInt('0x' + strArr[0]+strArr[1])
    colorArr[1] = parseInt('0x' + strArr[2]+strArr[3])
    colorArr[2] = parseInt('0x' + strArr[4]+strArr[5])
  }
  return colorArr
}
async function main(){
  const filterColor = hexToNum("#0097dc")
  let imgWidth=0,imgHeight=0,imgPix=0;
  const canvas = document.querySelector("#canvas") as HTMLCanvasElement
  const context = canvas.getContext('2d') as CanvasRenderingContext2D
  const image = new Image()
  image.src = IMG_URL
  const {promise,resolve} = withResolvers<void>()
  image.onload=()=>{
    resolve?.(undefined)
  }
  await promise;
  imgWidth = image.width
  imgHeight=image.height
  imgPix = imgWidth/imgHeight
  
  canvas.width = imgWidth
  canvas.height = image.height
  canvas.style.width = imgWidth / 10 + 'px'
  canvas.style.height = imgHeight / 10 + 'px'

  context.drawImage(image, 0, 0, imgWidth, imgHeight)
  const imgData = context.getImageData(0, 0, imgWidth, imgHeight)
  const data = imgData.data
  const step = 130;
  for(let i = 0; i< data.length;i += 4) {
    if(
      filterColor[0]+50>data[i+0]&&filterColor[0]-50 <data[i+0]&&
      filterColor[1]+step>data[i+1]&&filterColor[1]-step<data[i+1]&&
      filterColor[2]+step>data[i+2]&&filterColor[2]-step<data[i+2]
    ) {
      data[i+0] = 255
      data[i+1] = 255
      data[i+2] = 255
    }
  }
  context.putImageData(imgData, 0, 0)

  document.body.appendChild(canvas)

}

window.onload=()=>{
  main()
}

html

<body>
  <canvas id="canvas"></canvas>
  <script src="./src/index.ts"></script>
</body>

类形式书写

  • 思考:图片和canvas1是两种不同的对象,下面这种方式只是满足能用,但是如果我们将其理解为资源和处理,就会发现这个可以使用桥接模式来实现桥接模式定义:将对象的抽象部分和实现部分分离,使其能职责分离,独立变化(吃饭了,有时间再改)
function withResolvers<T>(){
  interface Obj{
    promise?:Promise<T>,
    resolve?:(value: T | PromiseLike<T>) => void
    reject?:(value: T | PromiseLike<T>) => void
  }
  const obj:Obj = {}
  obj.promise = new Promise<T>((res,rej)=>{
    obj.resolve=res;
    obj.reject=rej
  })
  return obj;
}
function hexToNum(s:string){
  let str = s
  const colorArr:number[] = []
  str = str.replace("#",'')
  const strArr = str.split("")
  if(strArr.length === 6) {
    colorArr[0] = parseInt('0x' + strArr[0]+strArr[1])
    colorArr[1] = parseInt('0x' + strArr[2]+strArr[3])
    colorArr[2] = parseInt('0x' + strArr[4]+strArr[5])
  }
  return colorArr
}

class ChangeImgBC {
  private image!:HTMLImageElement
  private imageSrc = ''
  private imageWidth=0
  private imageHeight = 0
  private canvas!:HTMLCanvasElement
  private context!:CanvasRenderingContext2D
  private filterColor:number[] = []
  constructor(imageSrc: string,canvas:HTMLCanvasElement,filterColor:string) {
    this.imageSrc = imageSrc
    this.canvas = canvas
    this.filterColor=hexToNum(filterColor)
    this.init()
  }
  async init (){
    await this.initImage()
    this.initCanvas()
  }
  private async initImage(){
    this.image =new Image()
    this.image.src = this.imageSrc
    const {promise,resolve} = withResolvers<void>()
    this.image.onload=()=>{
      resolve?.(undefined)
    }
    await promise;
    this.imageWidth = this.image.width
    this.imageHeight = this.image.height
  }
  private initCanvas() {
    this.context = this.canvas.getContext('2d') as CanvasRenderingContext2D
    this.canvas.width = this.imageWidth
    this.canvas.height = this.imageHeight
    this.canvas.style.width = this.imageWidth / 10 + 'px'
    this.canvas.style.height = this.imageHeight / 10 + 'px'
  }
  public handler(){
    this.context.drawImage(this.image, 0, 0, this.imageWidth, this.imageHeight)
    const imgData = this.context.getImageData(0, 0, this.imageWidth, this.imageHeight)
    const data = imgData.data
    const step = 130;
    for(let i = 0; i< data.length;i += 4) {
      if(
        this.filterColor[0]+50>data[i+0]&&  this.filterColor[0]-50 <data[i+0]&&
        this.filterColor[1]+step>data[i+1]&&this.filterColor[1]-step<data[i+1]&&
        this.filterColor[2]+step>data[i+2]&&this.filterColor[2]-step<data[i+2]
      ) {
        data[i+0] = 255
        data[i+1] = 255
        data[i+2] = 255
      }
    }
    this.context.putImageData(imgData, 0, 0)
  }
  public getImg() {
    return this.canvas.toDataURL("image/png")
  }
}

环境

vite+node-16

目录结构

├─node_modules      
├─public
│  └─img 
├─index.d.ts
├─index.html
└─src
   └─index.ts