How to simulate the background image style of css in canvas

The author has open sourced a web mind-map mind-map . Recently, I encountered a problem when optimizing the effect of the background image. When displayed on the page, the background image is rendered by cssusing , but when it is exported, it is actually drawn to the top. Then there will be a problem, the background image of the background image supports richer effects, such as setting the size, position, and repetition, but the author only found one method, and only supports setting the repetition effect, so how to simulate a certain As for the background effect, don't go away, let's try it together next.background-imagecanvascssbackground-sizebackground-positionbackground-repeatcanvascreatePattern()canvascss

The first thing to explain is that it will not 100%simulate cssall the effects perfectly and completely, because it cssis too powerful, the combination of attribute values ​​is very flexible, and there are many types, among which there are many types of units, so only some common situations will be simulated, and only units will be considered. pxand %.

After reading this article, you can also review the canvasmethod drawImageand cssthe usage of several attributes set in the background by the way.

The drawImage() method of canvas

In general, we will use canvasthe drawImage()method to draw the background image. Let's take a look at this method first. This method receives more parameters:

Only three parameters are required.

Basic framework and tools

The core logic is to load the picture, and then use drawImagethe method to draw the picture, which is nothing more than parameters csscalculated according to various attributes and values drawImage, so the following basic framework of the function can be written:

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)
  }
}

Next, let's look at a few tool functions.

// 将以空格分隔的字符串值转换成成数字/单位/值数组
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
    }
  })
}

cssThe attribute value of is a string or number type, for example 100px 100% auto, it is not convenient to use directly, so it is converted into [[100, 'px'], [100, '%'], 'auto']a form.

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

// 缩放高度
const zoomHeight = (ratio, width) => {
    
    
  // width / h = ratio
  return width / ratio
}

Calculate the scaled width or height based on the original ratio and the new width or height.

Simulate the background-size property

The default background-repeatvalue is repeat, we don't consider the case of duplication, so set it to no-repeat.

background-sizeThe attribute is used to set the size of the background image, and can accept four types of values, which are simulated in turn.

length type

Set the height and width of the background image. The first value sets the width and the second sets the height. If only one value is given, the second defaults to auto (automatic).

cssThe style is as follows:

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

If only one value is set, it represents the actual width of the background image display. If the height is not set, it will be automatically scaled according to the aspect ratio of the image. The effect is as follows:

The simulation in canvasis very simple, and four parameters need to be passed to drawImagethe method: img、x、y、width、height, imgwhich represents the image, x、yand represents the position of placing the image on the canvas. There is no special setting. Obviously, it means that 0、0the width、heightimage is scaled to the specified size. If background-sizeonly one value is passed, then widthSet it directly to this value, and heightcalculate according to the aspect ratio of the image. If two values ​​are passed, then pass the two values ​​separately width、height. In addition, you need to autoprocess the value, as follows:

drawBackgroundImageToCanvas(ctx, width, height, this.img, {
    
    
    backgroundSize: '300px'
})

const drawBackgroundImageToCanvas = () =>{
    
    
    // ...
    image.onload = () => {
    
    
        // ...
        // 模拟background-size
        handleBackgroundSize({
    
    
            backgroundSize, 
            drawOpt, 
            imageRatio
        })
        // ...
    }
}

// 模拟background-size
const 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)
      }
    }
}

The effect is as follows:

The effect of setting two values:

background-size: 300px 400px;

percentage type

The percentage of the localized area relative to the background will be calculated. The first value sets the width percentage, the second value sets the height percentage. If only one value is given, the second defaults to auto (automatic). For example, if it is set 50% 80%, it means that the image will be scaled to 50%the width and 80%height of the background area.

cssThe style is as follows:

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

The implementation is also very simple. On the basis of the above, judge whether the unit is %, if yes, canvascalculate the width and height of the picture to be displayed according to the width and height of the picture. The second value is not set or is, autoas before, it is also based on the aspect ratio of the picture to adapt.

drawBackgroundImageToCanvas(ctx, width, height, this.img, {
    
    
    backgroundSize: '50% 80%'
})

handleBackgroundSize({
    
    
    backgroundSize,
    drawOpt,
    imageRatio,
    canvasWidth: width,// 传参新增canvas的宽高
    canvasHeight: height
})

// 模拟background-size
const 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)
    }
  }
}

The effect is as follows:

cover type

background-sizeSet to mean that coverthe image will keep its original aspect ratio, and scaled to the minimum size that will completely cover the background positioning area. Note that the image will not be deformed.

cssThe style is as follows:

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

This implementation is also very simple. According to the aspect ratio of the picture and canvasthe aspect ratio of the picture, whether the width of the zoomed picture is canvasthe same as the width of the picture, or the height of the picture canvasis the same as the height of the picture.

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
    }
    // ...
}

The effect is as follows:

contain type

background-sizeSetting it to containtype means that the picture will still maintain the original aspect ratio, and it will be scaled to the maximum size suitable for the background positioning area, that is, the picture will be displayed completely, but it will not necessarily cover the background horizontally and vertically. There may be blank space in one direction.

cssstyle:

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

The implementation coveris just the opposite of the implementation of the type. If the aspect ratio of the picture is greater than the canvasaspect ratio of the picture, in order to make the picture fully displayed, the width of the picture canvasis consistent with the width of the picture, and the height is self-adaptive.

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
    }
}

The effect is as follows:

background-sizeThe simulation here is over, let's take a look background-position.

Simulate the background-position property

First look at background-sizethe situation where it is not set.

background-positionThe property is used to set the starting position of the background image, the default value is 0% 0%, it also supports several different types of values, see them one by one.

percentage type

The first value sets the horizontal position and the second value sets the vertical position. The upper left corner is 0%0%, the lower right corner is 100%100%, if only one value is set, the second default is 50%, for example, it is set to 50% 60%, which means aligning 50% 60%the position of the picture with the position of the background area , and for example , representing the center point of the picture and the center of the background area points coincide.50% 60%50% 50%

cssstyle:

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

In terms of implementation, we only need to use the three parameters of drawImagethe method . The width and height of the picture will not be scaled, and the distance corresponding to the picture is calculated according to the ratio . Their difference is the position of the picture displayed on the picture.imgx、ycanvascanvas

drawBackgroundImageToCanvas(ctx, width, height, this.img, {
    
    
    backgroundPosition: '50% 50%'
})

const drawBackgroundImageToCanvas = () => {
    
    
    // ...
    // 模拟background-position
    handleBackgroundPosition({
    
    
      backgroundPosition,
      drawOpt,
      imgWidth,
      imgHeight,
      canvasWidth: width,
      canvasHeight: height
    })
    // ...
}

// 模拟background-position
const 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
      }
    }
  }
}

The effect is as follows:

length type

The first value represents the horizontal position and the second value represents the vertical position. The upper left corner is 0 0. The unit can be pxor any other cssunit, of course, we only consider px. If only one value is specified, the others will be 50%. So you can mix %and match px.

cssstyle:

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

This implementation is simpler, just pass the value directly to drawImagethe x、yparameter.

drawBackgroundImageToCanvas(ctx, width, height, this.img, {
    
    
    backgroundPosition: '50px 150px'
})

// 模拟background-position
const handleBackgroundPosition = ({
     
     }) => {
    
    
    // ...
    // 水平位置
    if (backgroundPositionValueArr[0][1] === '%') {
    
    
        // ...
    } else {
    
    
        // 其他单位默认都为px
        drawOpt.x = backgroundPositionValueArr[0][0]
    }
    // 垂直位置
    if (backgroundPositionValueArr[1][1] === '%') {
    
    
        // ...
    } else {
    
    
        // 其他单位默认都为px
        drawOpt.y = backgroundPositionValueArr[1][0]
    }
}

keyword type

That is to combine keywords such as , left, and so on. It can be regarded as a special value, so we only need to write a mapping to map these keywords to percentage values.topleft 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
    })
    // ...
}

Combined with background-size

Finally we look at background-sizewhat happens when combined with and .

.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'
})

The result is as follows:

Inconsistent, why is this? Let's sort it out. First, the processing background-sizewill calculate drawImagethe parameters width、height, that is, canvasthe width and height of the picture displayed in it, and background-positionthe width and height of the picture will be used in the processing, but what we pass is still the picture The original width and height of , of course there is a problem with this calculation, modify it:

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

Now look at the effect again:

Simulate the background-repeat property

background-repeatThe attribute is used to set how to tile the object background-image. The default value repeatis, that is, when the image is smaller than the background area, it will repeat vertically and horizontally by default. There are several optional values:

  • repeat-x: only horizontal position will repeat the background image
  • repeat-y: Only the vertical position will repeat the background image
  • no-repeat: background-imagewill not repeat

Next, we implement these situations.

no-repeat

Firstly, judge whether the width and height of the image are larger than the background area. If so, there is no need to tile or process, and the other value does not no-repeatneed to be processed:

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

You can see that the width and height of the picture we upload here is also the calculated background-sizepicture display width and height.

// 模拟background-repeat
const 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

Next, add repeat-xsupport for the pair. When canvasthe width of the image is greater than the width of the picture, then the horizontal tile is drawn, and the drawing will call the drawImagemethod repeatedly, so it is necessary to pass ctxand imageparameters to handleBackgroundRepeatthe method. In addition, if handleBackgroundRepeatthe drawing is performed in the method, the original drawing method There is no need to call:

// 模拟background-repeat
// 如果在handleBackgroundRepeat里进行了绘制,那么会返回true
let 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
  )
}

The drawing method is extracted into a method for easy reuse.

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
      }
    }
    // ...
}

Each time the image's placement position xparameter is updated until canvasthe width exceeds.

repeat-y

The right repeat-yhandling is similar:

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

And finally repeatthe value, which is repeated both horizontally and vertically:

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
    }
}

From left to right, it is drawn column by column, horizontally to the xexcess canvaswidth, and vertically to ythe excess canvasheight.

Combination with background-size, background-position

Finally, look at the combination with the first two attributes.

cssstyle:

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

The effect is as follows:

The size of the picture is correct, but the position is incorrect. The cssbest way to do it is to first background-positionposition a picture according to the value of , and then tile it around, but we obviously ignore this situation and 0 0start drawing from the position every time.

Knowing the principle, the solution is also very simple. It handleBackgroundPositionhas been calculated in the method x、y, that is, the placement position of the first picture before tiling:

We only need to calculate how many pictures can be tiled on the left and the top, and calculate the position of the first picture in the horizontal and vertical directions as the x、yinitial value of the subsequent cycle.

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
          // ...
        }
      }
    }
}

end

This article simply realizes some of the effects of the three attributes canvassimulated in , , and . The complete source code cssis at https://github.com/wanglin2/simulateCSSBackgroundInCanvas .background-sizebackground-positionbackground-repeat

Guess you like

Origin blog.csdn.net/sinat_33488770/article/details/129253709