笔者开源了一个Web思维导图mind-map,最近在优化背景图片成果的时候遇到了一个问题,页面上展现时背景图片是通过css
应用background-image
渲染的,而导出的时候实际上是绘制到canvas
上导出的,那么就会有个问题,css
的背景图片反对比拟丰盛的成果,比方通过background-size
设置大小,通过background-position
设置地位,通过background-repeat
设置反复,然而canvas
笔者只找到一个createPattern()
办法,且只反对设置反复成果,那么如何在canvas
里模仿肯定的css
背景成果呢,不要走开,接下来一起来试试。
首先要阐明的是不会去完满残缺100%
模仿css
的所有成果,因为css
太强大了,属性值组合很灵便,且品种十分多,其中单位就很多种,所有只会模仿一些常见的状况,单位也只思考px
和%
。
读完本文,你还能够顺便温习一下canvas
的drawImage
办法,以及css
背景设置的几个属性的用法。
canvas的drawImage()办法
总的来说,咱们会应用canvas
的drawImage()
办法来绘制背景图片,先来大抵看一下这个办法,这个办法接管的参数比拟多:
只有三个参数是必填的。
根本框架和工具办法
外围逻辑就是加载图片,而后应用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、height
,img
代表图片,x、y
代表在画布上搁置图片的地位,没有非凡设置,显然就是0、0
,width、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-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)
}
}
}
成果如下:
设置两个值的成果:
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-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)
}
}
}
成果如下:
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
办法的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
}
}
}
}
成果如下:
length类型
第一个值代表程度地位,第二个值代表垂直地位。左上角是0 0
。单位能够是px
或任何其余css
单位,当然,咱们只思考px
。如果仅指定了一个值,其余值将是50%
。所以你能够混合应用%
和px
。
css
款式:
.cssBox {
background-image: url('/2.jpg');
background-repeat: no-repeat;
background-position: 50px 150px;
}
这个实现更简略,间接把值传给drawImage
的x、y
参数即可。
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]
}
}
关键词类型
也就是通过left
、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
})
// ...
}
和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-position
handleBackgroundPosition({
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-repeat
:background-image
不会反复
接下来咱们实现一下这几种状况。
no-repeat
首先判断图片的宽高是否都比背景区域大,是的话就不须要平铺,也就不必解决,另外值为no-repeat
也不须要做解决:
// 模仿background-repeat
handleBackgroundRepeat({
backgroundRepeat,
drawOpt,
imgWidth: drawOpt.width,
imgHeight: drawOpt.height,
imageRatio,
canvasWidth: width,
canvasHeight: height,
canvasRatio
})
能够看到这里咱们传的图片的宽高也是经background-size
计算后的图片显示宽高。
// 模仿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
接下来减少对repeat-x
的反对,当canvas
的宽度大于图片的宽度,那么程度平铺进行绘制,绘制会反复调用drawImage
办法,所以还须要再传递ctx
、image
参数给handleBackgroundRepeat
办法,另外如果handleBackgroundRepeat
办法里进行了绘制,原来的绘制办法就不必再调用了:
// 模仿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
)
}
将绘制的办法提取成了一个办法,不便复用。
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
中模仿css
的background-size
、background-position
、background-repeat
三个属性的局部成果,残缺源码在https://github.com/wanglin2/simulateCSSBackgroundInCanvas。
发表回复