笔者开源了一个Web思维导图mind-map,最近在优化背景图片成果的时候遇到了一个问题,页面上展现时背景图片是通过css应用background-image渲染的,而导出的时候实际上是绘制到canvas上导出的,那么就会有个问题,css的背景图片反对比拟丰盛的成果,比方通过background-size设置大小,通过background-position设置地位,通过background-repeat设置反复,然而canvas笔者只找到一个createPattern()办法,且只反对设置反复成果,那么如何在canvas里模仿肯定的css背景成果呢,不要走开,接下来一起来试试。

首先要阐明的是不会去完满残缺100%模仿css的所有成果,因为css太强大了,属性值组合很灵便,且品种十分多,其中单位就很多种,所有只会模仿一些常见的状况,单位也只思考px%

读完本文,你还能够顺便温习一下canvasdrawImage办法,以及css背景设置的几个属性的用法。

canvas的drawImage()办法

总的来说,咱们会应用canvasdrawImage()办法来绘制背景图片,先来大抵看一下这个办法,这个办法接管的参数比拟多:

只有三个参数是必填的。

根本框架和工具办法

外围逻辑就是加载图片,而后应用drawImage办法绘制图片,无非是依据各种css的属性和值来计算drawImage的参数,所以能够写出上面的函数根本框架:

const drawBackgroundImageToCanvas = (  ctx,// canvas绘图上下文  width,// canvas宽度  height,// canvas高度  img,// 图片url  { backgroundSize, backgroundPosition, backgroundRepeat }// css款式,只模仿这三种) => {  // canvas的宽高比  let canvasRatio = width / height  // 加载图片  let image = new Image()  image.src = img  image.onload = () => {    // 图片的宽高及宽高比    let imgWidth = image.width    let imgHeight = image.height    let imageRatio = imgWidth / imgHeight    // 绘制图片    // drawImage办法的参数值    let drawOpt = {        sx: 0,        sy: 0,        swidth: imgWidth,// 默认绘制残缺图片        sheight: imgHeight,        x: 0,        y: 0,        width: imgWidth,// 默认不缩放图片        height: imgHeight    }    // 依据css属性和值计算...    // 绘制图片    ctx.drawImage(image, drawOpt.sx, drawOpt.sy, drawOpt.swidth, drawOpt.sheight, drawOpt.x, drawOpt.y, drawOpt.width, drawOpt.height)  }}

接下来看几个工具函数。

// 将以空格分隔的字符串值转换成成数字/单位/值数组const getNumberValueFromStr = value => {  let arr = String(value).split(/\s+/)  return arr.map(item => {    if (/^[\d.]+/.test(item)) {        // 数字+单位        let res = /^([\d.]+)(.*)$/.exec(item)        return [Number(res[1]), res[2]]    } else {        // 单个值        return item    }  })}

css的属性值为字符串或数字类型,比方100px 100% auto,不不便间接应用,所以转换成[[100, 'px'], [100, '%'], 'auto']模式。

// 缩放宽度const zoomWidth = (ratio, height) => {    // w / height = ratio    return ratio * height}// 缩放高度const zoomHeight = (ratio, width) => {  // width / h = ratio  return width / ratio}

依据原比例和新的宽度或高度,计算缩放后的宽度或高度。

模仿background-size属性

默认background-repeat的值为repeat,咱们先不思考反复的状况,所以先把它设置成no-repeat

background-size 属性用于设置背景图片的大小,能够承受四种类型的值,顺次来模仿一下。

length类型

设置背景图片的高度和宽度。第一个值设置宽度,第二个值设置高度。如果只给出一个值,第二个默认为 auto(主动)。

css款式如下:

.cssBox {    background-image: url('/1.jpg');    background-repeat: no-repeat;    background-size: 300px;}

只设置一个值,那么代表背景图片显示的理论宽度,高度没有设置,那么会依据图片的长宽比主动缩放,成果如下:

canvas中模仿很简略,须要传给drawImage办法四个参数:img、x、y、width、heightimg代表图片,x、y代表在画布上搁置图片的地位,没有非凡设置,显然就是0、0width、height代表将图片缩放到指定大小,如果background-size只传了一个值,那么width间接设置成这个值,而height则依据图片的长宽比进行计算,如果传了两个值,那么别离把两个值传给width、height即可,另外须要对值为auto的进行一下解决,实现如下:

drawBackgroundImageToCanvas(ctx, width, height, this.img, {    backgroundSize: '300px'})const drawBackgroundImageToCanvas = () =>{    // ...    image.onload = () => {        // ...        // 模仿background-size        handleBackgroundSize({            backgroundSize,             drawOpt,             imageRatio        })        // ...    }}// 模仿background-sizeconst handleBackgroundSize = ({ backgroundSize, drawOpt, imageRatio }) => {    if (backgroundSize) {      // 将值转换成数组      let backgroundSizeValueArr = getNumberValueFromStr(backgroundSize)      // 两个值都为auto,那就相当于不设置      if (backgroundSizeValueArr[0] === 'auto' && backgroundSizeValueArr[1] === 'auto') {        return      }      // 图片宽度      let newNumberWidth = -1      if (backgroundSizeValueArr[0]) {        if (Array.isArray(backgroundSizeValueArr[0])) {            // 数字+单位类型            drawOpt.width = backgroundSizeValueArr[0][0]            newNumberWidth = backgroundSizeValueArr[0][0]        } else if (backgroundSizeValueArr[0] === 'auto') {            // auto类型,那么依据设置的新高度以图片原宽高比进行自适应            if (backgroundSizeValueArr[1]) {                drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0])            }        }      }      // 设置了图片高度      if (backgroundSizeValueArr[1] && Array.isArray(backgroundSizeValueArr[1])) {        // 数字+单位类型        drawOpt.height = backgroundSizeValueArr[1][0]      } else if (newNumberWidth !== -1) {        // 没有设置图片高度或者设置为auto,那么依据设置的新宽度以图片原宽高比进行自适应        drawOpt.height = zoomHeight(imageRatio, newNumberWidth)      }    }}

成果如下:

设置两个值的成果:

background-size: 300px 400px;

percentage类型

将计算绝对于背景定位区域的百分比。第一个值设置宽度百分比,第二个值设置的高度百分比。如果只给出一个值,第二个默认为auto(主动)。比方设置了50% 80%,意思是将图片缩放到背景区域的50%宽度和80%高度。

css款式如下:

.cssBox {    background-image: url('/1.jpg');    background-repeat: no-repeat;    background-size: 50% 80%;}

实现也很简略,在后面的根底上判断一下单位是否是%,是的话就依照canvas的宽高来计算图片要显示的宽高,第二值没有设置或者为auto,跟之前一样也是依据图片的宽高比来自适应。

drawBackgroundImageToCanvas(ctx, width, height, this.img, {    backgroundSize: '50% 80%'})handleBackgroundSize({    backgroundSize,    drawOpt,    imageRatio,    canvasWidth: width,// 传参新增canvas的宽高    canvasHeight: height})// 模仿background-sizeconst handleBackgroundSize = ({ backgroundSize, drawOpt, imageRatio, canvasWidth, canvasHeight }) => {  if (backgroundSize) {    // ...    // 图片宽度    let newNumberWidth = -1    if (backgroundSizeValueArr[0]) {      if (Array.isArray(backgroundSizeValueArr[0])) {        // 数字+单位类型        if (backgroundSizeValueArr[0][1] === '%') {            // %单位,则图片显示的高度为画布的百分之多少            drawOpt.width = backgroundSizeValueArr[0][0] / 100 * canvasWidth            newNumberWidth = drawOpt.width        } else {            // 其余都认为是px单位            drawOpt.width = backgroundSizeValueArr[0][0]            newNumberWidth = backgroundSizeValueArr[0][0]        }      } else if (backgroundSizeValueArr[0] === 'auto') {        // auto类型,那么依据设置的新高度以图片原宽高比进行自适应        if (backgroundSizeValueArr[1]) {            if (backgroundSizeValueArr[1][1] === '%') {                // 高度为%单位                drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0] / 100 * canvasHeight)            } else {                // 其余都认为是px单位                drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0])            }        }      }    }    // 设置了图片高度    if (backgroundSizeValueArr[1] && Array.isArray(backgroundSizeValueArr[1])) {      // 数字+单位类型      if (backgroundSizeValueArr[1][1] === '%') {        // 高度为%单位        drawOpt.height = backgroundSizeValueArr[1][0] / 100 * canvasHeight      } else {        // 其余都认为是px单位        drawOpt.height = backgroundSizeValueArr[1][0]      }    } else if (newNumberWidth !== -1) {      // 没有设置图片高度或者设置为auto,那么依据设置的新宽度以图片原宽高比进行自适应      drawOpt.height = zoomHeight(imageRatio, newNumberWidth)    }  }}

成果如下:

cover类型

background-size设置为cover代表图片会放弃原来的宽高比,并且缩放成将齐全笼罩背景定位区域的最小大小,留神,图片不会变形。

css款式如下:

.cssBox {    background-image: url('/3.jpeg');    background-repeat: no-repeat;    background-size: cover;}

这个实现也很简略,依据图片的宽高比和canvas的宽高比判断,到底是缩放图片的宽度和canvas的宽度统一,还是缩放图片的高度和canvas的高度一致。

drawBackgroundImageToCanvas(ctx, width, height, this.img, {    backgroundSize: 'cover'})handleBackgroundSize({    backgroundSize,    drawOpt,    imageRatio,    canvasWidth: width,    canvasHeight: height,    canvasRatio// 参数减少canvas的宽高比})const handleBackgroundSize = ({  backgroundSize,  drawOpt,  imageRatio,  canvasWidth,  canvasHeight,  canvasRatio}) => {    // ...    // 值为cover    if (backgroundSizeValueArr[0] === 'cover') {        if (imageRatio > canvasRatio) {            // 图片的宽高比大于canvas的宽高比,那么图片高度缩放到和canvas的高度一致,宽度自适应            drawOpt.height = canvasHeight            drawOpt.width = zoomWidth(imageRatio, canvasHeight)        } else {            // 否则图片宽度缩放到和canvas的宽度统一,高度自适应            drawOpt.width = canvasWidth            drawOpt.height = zoomHeight(imageRatio, canvasWidth)        }        return    }    // ...}

成果如下:

contain类型

background-size设置为contain类型示意图片还是会放弃原有的宽高比,并且缩放成适宜背景定位区域的最大大小,也就是图片会显示残缺,然而不肯定会铺满背景的程度和垂直两个方向,在某个方向可能会有留白。

css款式:

.cssBox {    background-image: url('/1.jpg');    background-repeat: no-repeat;    background-size: contain;}

实现刚好和cover类型的实现反过来即可,如果图片的宽高比大于canvas的宽高比,为了让图片显示齐全,让图片的宽度和canvas的宽度统一,高度自适应。

const handleBackgroundSize = () => {    // ...    // 值为contain    if (backgroundSizeValueArr[0] === 'contain') {        if (imageRatio > canvasRatio) {            // 图片的宽高比大于canvas的宽高比,那么图片宽度缩放到和canvas的宽度统一,高度自适应            drawOpt.width = canvasWidth            drawOpt.height = zoomHeight(imageRatio, canvasWidth)        } else {            // 否则图片高度缩放到和canvas的高度一致,宽度自适应            drawOpt.height = canvasHeight            drawOpt.width = zoomWidth(imageRatio, canvasHeight)        }        return    }}

成果如下:

到这里对background-size的模仿就完结了,接下来看看background-position

模仿background-position属性

先看不设置background-size的状况。

background-position属性用于设置背景图像的起始地位,默认值为 0% 0%,它也反对几种不同类型的值,一一来看。

percentage类型

第一个值设置程度地位,第二个值设置垂直地位。左上角是0%0%,右下角是100%100%,如果只设置了一个值,第二个默认为50%,比方设置为50% 60%,意思是将图片的50% 60%地位和背景区域的50% 60%地位进行对齐,又比方50% 50%,代表图片中心点和背景区域中心点重合。

css款式:

.cssBox {    background-image: url('/2.jpg');    background-repeat: no-repeat;    background-position: 50% 50%;}

实现上咱们只须要用到drawImage办法的imgx、y三个参数,图片的宽高不会进行缩放,依据比例别离算出在canvas和图片上对应的间隔,他们的差值即为图片在canvas上显示的地位。

drawBackgroundImageToCanvas(ctx, width, height, this.img, {    backgroundPosition: '50% 50%'})const drawBackgroundImageToCanvas = () => {    // ...    // 模仿background-position    handleBackgroundPosition({      backgroundPosition,      drawOpt,      imgWidth,      imgHeight,      canvasWidth: width,      canvasHeight: height    })    // ...}// 模仿background-positionconst handleBackgroundPosition = ({  backgroundPosition,  drawOpt,  imgWidth,  imgHeight,  canvasWidth,  canvasHeight}) => {  if (backgroundPosition) {    // 将值转换成数组    let backgroundPositionValueArr = getNumberValueFromStr(backgroundPosition)    if (Array.isArray(backgroundPositionValueArr[0])) {      if (backgroundPositionValueArr.length === 1) {        // 如果只设置了一个值,第二个默认为50%        backgroundPositionValueArr.push([50, '%'])      }      // 程度地位      if (backgroundPositionValueArr[0][1] === '%') {        // 单位为%        let canvasX = (backgroundPositionValueArr[0][0] / 100) * canvasWidth        let imgX = (backgroundPositionValueArr[0][0] / 100) * imgWidth        // 计算差值        drawOpt.x = canvasX - imgX      }      // 垂直地位      if (backgroundPositionValueArr[1][1] === '%') {        // 单位为%        let canvasY = (backgroundPositionValueArr[1][0] / 100) * canvasHeight        let imgY = (backgroundPositionValueArr[1][0] / 100) * imgHeight        // 计算差值        drawOpt.y = canvasY - imgY      }    }  }}

成果如下:

length类型

第一个值代表程度地位,第二个值代表垂直地位。左上角是0 0。单位能够是px或任何其余css单位,当然,咱们只思考px。如果仅指定了一个值,其余值将是50%。所以你能够混合应用%px

css款式:

.cssBox {    background-image: url('/2.jpg');    background-repeat: no-repeat;    background-position: 50px 150px;}

这个实现更简略,间接把值传给drawImagex、y参数即可。

drawBackgroundImageToCanvas(ctx, width, height, this.img, {    backgroundPosition: '50px 150px'})// 模仿background-positionconst handleBackgroundPosition = ({}) => {    // ...    // 程度地位    if (backgroundPositionValueArr[0][1] === '%') {        // ...    } else {        // 其余单位默认都为px        drawOpt.x = backgroundPositionValueArr[0][0]    }    // 垂直地位    if (backgroundPositionValueArr[1][1] === '%') {        // ...    } else {        // 其余单位默认都为px        drawOpt.y = backgroundPositionValueArr[1][0]    }}

关键词类型

也就是通过lefttop之类的关键词进行组合,比方:left topcenter centercenter bottom等。能够看做是非凡的%值,所以咱们只有写一个映射将这些关键词对应上百分比数值即可。

.cssBox {    background-image: url('/2.jpg');    background-repeat: no-repeat;    background-position: right bottom;}
drawBackgroundImageToCanvas(ctx, width, height, this.img, {    backgroundPosition: 'right bottom'})// 关键词到百分比值的映射const keyWordToPercentageMap = {  left: 0,  top: 0,  center: 50,  bottom: 100,  right: 100}const handleBackgroundPosition = ({}) => {    // ...    // 将关键词转为百分比    backgroundPositionValueArr = backgroundPositionValueArr.map(item => {      if (typeof item === 'string') {        return keyWordToPercentageMap[item] !== undefined          ? [keyWordToPercentageMap[item], '%']          : item      }      return item    })    // ...}

和background-size组合

最初咱们来看看和background-size组合应用会产生什么状况。

.cssBox {    background-image: url('/2.jpg');    background-repeat: no-repeat;    background-size: cover;    background-position: right bottom;}
drawBackgroundImageToCanvas(ctx, width, height, this.img, {    backgroundSize: 'cover',    backgroundPosition: 'right bottom'})

后果如下:

不统一,这是为啥呢,咱们来梳理一下,首先在解决background-size会计算出drawImage参数中的width、height,也就是图片在canvas中显示的宽高,而在解决background-position时会用到图片的宽高,然而咱们传的还是图片的原始宽高,这样计算出来当然是有问题的,批改一下:

// 模仿background-positionhandleBackgroundPosition({    backgroundPosition,    drawOpt,    imgWidth: drawOpt.width,// 改为传计算后的图片的显示宽高    imgHeight: drawOpt.height,    imageRatio,    canvasWidth: width,    canvasHeight: height,    canvasRatio})

当初再来看看成果:

模仿background-repeat属性

background-repeat属性用于设置如何平铺对象的background-image属性,默认值为repeat,也就是当图片比背景区域小时默认会向垂直和程度方向反复,另外还有几个可选值:

  • repeat-x:只有程度地位会反复背景图像
  • repeat-y:只有垂直地位会反复背景图像
  • no-repeatbackground-image不会反复

接下来咱们实现一下这几种状况。

no-repeat

首先判断图片的宽高是否都比背景区域大,是的话就不须要平铺,也就不必解决,另外值为no-repeat也不须要做解决:

// 模仿background-repeathandleBackgroundRepeat({    backgroundRepeat,    drawOpt,    imgWidth: drawOpt.width,    imgHeight: drawOpt.height,    imageRatio,    canvasWidth: width,    canvasHeight: height,    canvasRatio})

能够看到这里咱们传的图片的宽高也是经background-size计算后的图片显示宽高。

// 模仿background-repeatconst handleBackgroundRepeat = ({  backgroundRepeat,  drawOpt,  imgWidth,  imgHeight,  canvasWidth,  canvasHeight,}) => {    if (backgroundRepeat) {        // 将值转换成数组        let backgroundRepeatValueArr = getNumberValueFromStr(backgroundRepeat)        // 不解决        if (backgroundRepeatValueArr[0] === 'no-repeat' || (imgWidth >= canvasWidth && imgHeight >= canvasHeight)) {            return        }    }}

repeat-x

接下来减少对repeat-x的反对,当canvas的宽度大于图片的宽度,那么程度平铺进行绘制,绘制会反复调用drawImage办法,所以还须要再传递ctximage参数给handleBackgroundRepeat办法,另外如果handleBackgroundRepeat办法里进行了绘制,原来的绘制办法就不必再调用了:

// 模仿background-repeat// 如果在handleBackgroundRepeat里进行了绘制,那么会返回truelet notNeedDraw = handleBackgroundRepeat({    ctx,    image,    ...})if (!notNeedDraw) {    drawImage(ctx, image, drawOpt)}// 依据参数绘制图片const drawImage = (ctx, image, drawOpt) => {  ctx.drawImage(    image,    drawOpt.sx,    drawOpt.sy,    drawOpt.swidth,    drawOpt.sheight,    drawOpt.x,    drawOpt.y,    drawOpt.width,    drawOpt.height  )}

将绘制的办法提取成了一个办法,不便复用。

const handleBackgroundRepeat = ({}) => {    // ...    // 程度平铺    if (backgroundRepeatValueArr[0] === 'repeat-x') {      if (canvasWidth > imgWidth) {        let x = 0        while (x < canvasWidth) {          drawImage(ctx, image, {            ...drawOpt,            x          })          x += imgWidth        }        return true      }    }    // ...}

每次更新图片的搁置地位x参数,直到超出canvas的宽度。

repeat-y

repeat-y的解决也是相似的:

const handleBackgroundRepeat = ({}) => {    // ...    // 垂直平铺    if (backgroundRepeatValueArr[0] === 'repeat-y') {      if (canvasHeight > imgHeight) {        let y = 0        while (y < canvasHeight) {          drawImage(ctx, image, {            ...drawOpt,            y          })          y += imgHeight        }        return true      }    }    // ...}

repeat

最初就是repeat值,也就是程度和垂直都进行反复:

const handleBackgroundRepeat = ({}) => {    // ...    // 平铺    if (backgroundRepeatValueArr[0] === 'repeat') {      let x = 0      while (x < canvasWidth) {        if (canvasHeight > imgHeight) {          let y = 0          while (y < canvasHeight) {            drawImage(ctx, image, {              ...drawOpt,              x,              y            })            y += imgHeight          }        }        x += imgWidth      }      return true    }}

从左到右,一列一列进行绘制,程度方向绘制到x超出canvas的宽度为止,垂直方向绘制到y超出canvas的高度为止。

和background-size、background-position组合

最初同样看一下和前两个属性的组合状况。

css款式:

.cssBox {    background-image: url('/4.png');    background-repeat: repeat;    background-size: 50%;    background-position: 50% 50%;}

成果如下:

图片大小是正确的,然而地位不正确,css的做法应该是先依据background-position的值定位一张图片,而后再向周围进行平铺,而咱们显然疏忽了这种状况,每次都从0 0地位开始绘制。

晓得了原理,解决也很简略,在handleBackgroundPosition办法中曾经计算出了x、y,也就是没有平铺前第一张图片的搁置地位:

咱们只有计算出右边和上边还能平铺多少张图片,把程度和垂直方向上第一张图片的地位计算出来,作为后续循环的x、y的初始值即可。

const handleBackgroundRepeat = ({}) => {    // 保留在handleBackgroundPosition中计算出来的x、y    let ox = drawOpt.x    let oy = drawOpt.y    // 计算ox和oy能平铺的图片数量    let oxRepeatNum = Math.ceil(ox / imgWidth)    let oyRepeatNum = Math.ceil(oy / imgHeight)    // 计算ox和oy第一张图片的地位    let oxRepeatX = ox - oxRepeatNum * imgWidth     let oxRepeatY = oy - oyRepeatNum * imgHeight    // 将oxRepeatX和oxRepeatY作为后续循环的x、y的初始值    // ...    // 平铺    if (backgroundRepeatValueArr[0] === 'repeat') {      let x = oxRepeatX      while (x < canvasWidth) {        if (canvasHeight > imgHeight) {          let y = oxRepeatY          // ...        }      }    }}

结尾

本文简略实现了一下在canvas中模仿cssbackground-sizebackground-positionbackground-repeat三个属性的局部成果,残缺源码在https://github.com/wanglin2/simulateCSSBackgroundInCanvas。