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 css
using , 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-image
canvas
css
background-size
background-position
background-repeat
canvas
createPattern()
canvas
css
The first thing to explain is that it will not 100%
simulate css
all the effects perfectly and completely, because it css
is 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. px
and %
.
After reading this article, you can also review the canvas
method drawImage
and css
the usage of several attributes set in the background by the way.
The drawImage() method of canvas
In general, we will use canvas
the 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 drawImage
the method to draw the picture, which is nothing more than parameters css
calculated 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
}
})
}
css
The 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-repeat
value is repeat
, we don't consider the case of duplication, so set it to no-repeat
.
background-size
The 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).
css
The 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 canvas
is very simple, and four parameters need to be passed to drawImage
the method: img、x、y、width、height
, img
which represents the image, x、y
and represents the position of placing the image on the canvas. There is no special setting. Obviously, it means that 0、0
the width、height
image is scaled to the specified size. If background-size
only one value is passed, then width
Set it directly to this value, and height
calculate 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 auto
process 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.
css
The 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, canvas
calculate 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, auto
as 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-size
Set to mean that cover
the 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.
css
The 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 canvas
the aspect ratio of the picture, whether the width of the zoomed picture is canvas
the same as the width of the picture, or the height of the picture canvas
is 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-size
Setting it to contain
type 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.
css
style:
.cssBox {
background-image: url('/1.jpg');
background-repeat: no-repeat;
background-size: contain;
}
The implementation cover
is just the opposite of the implementation of the type. If the aspect ratio of the picture is greater than the canvas
aspect ratio of the picture, in order to make the picture fully displayed, the width of the picture canvas
is 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-size
The simulation here is over, let's take a look background-position
.
Simulate the background-position property
First look at background-size
the situation where it is not set.
background-position
The 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%
css
style:
.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 drawImage
the 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.img
x、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-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 px
or any other css
unit, of course, we only consider px
. If only one value is specified, the others will be 50%
. So you can mix %
and match px
.
css
style:
.cssBox {
background-image: url('/2.jpg');
background-repeat: no-repeat;
background-position: 50px 150px;
}
This implementation is simpler, just pass the value directly to drawImage
the x、y
parameter.
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.top
left top
center center
center 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-size
what 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-size
will calculate drawImage
the parameters width、height
, that is, canvas
the width and height of the picture displayed in it, and background-position
the 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-repeat
The attribute is used to set how to tile the object background-image
. The default value repeat
is, 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 imagerepeat-y
: Only the vertical position will repeat the background imageno-repeat
:background-image
will 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-repeat
need 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-size
picture 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-x
support for the pair. When canvas
the width of the image is greater than the width of the picture, then the horizontal tile is drawn, and the drawing will call the drawImage
method repeatedly, so it is necessary to pass ctx
and image
parameters to handleBackgroundRepeat
the method. In addition, if handleBackgroundRepeat
the 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 x
parameter is updated until canvas
the width exceeds.
repeat-y
The right repeat-y
handling 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 repeat
the 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 x
excess canvas
width, and vertically to y
the excess canvas
height.
Combination with background-size, background-position
Finally, look at the combination with the first two attributes.
css
style:
.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 css
best way to do it is to first background-position
position a picture according to the value of , and then tile it around, but we obviously ignore this situation and 0 0
start drawing from the position every time.
Knowing the principle, the solution is also very simple. It handleBackgroundPosition
has 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、y
initial 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 canvas
simulated in , , and . The complete source code css
is at https://github.com/wanglin2/simulateCSSBackgroundInCanvas .background-size
background-position
background-repeat