最简单粗暴的js方式实现background-size

1,978 阅读7分钟

最近有个需求,要求按照最小边展示图片,并且要求同一张图片,在客户端侧和前端侧展示的图片样式完全相同。
这就要求前端跟客户端的图片大小要完全一致,并且双方协商好按照同一方式进行展示。

当然,我们可以直接用background-size:cover;来解决,但是,background是毕竟不是img,如果想宽度固定,高度自适应还是比较难的,而直接用img,那么只能使用object-fit:cover;object-position:center;的方法,但是这种方法兼容性较差。

object-fit兼容性

object-fit 兼容性

所以,这里我们探讨一些其他的方法。

我们可以让div套住img,通过div限制大小,通过获取img的尺寸自适应div。

<div class="img_box">
    <img class="img" />
</div>

思路确定

如下图所示,左侧空白正方形为一个box,右侧矩形为图片。

  • 步骤一:新建一个box和一个图片;
  • 步骤二:可将图片最小边缩放至与box同样大小,然后将图片大小同比例缩放;
  • 步骤三:将图片移动至box中间位置。
    步骤1
    步骤2
    步骤3

问题分解

  • 问题1:根据宽高比例不同,需要进行不同的缩放,如何得到图片的宽与高呢?
  • 答:图片在不加载之前,是无法获取图片具体的尺寸的,那么就让图片先加载出来,先获得图片的尺寸,再根据图片尺寸类型进行展示。
let imgLoader = new Image();
let img = document.querySelector('.img'), box = document.querySelector('.img_box');
let imgSrc = './pic1.jpg';
imgLoader.onload = () => {
    let width = imgLoader.width, height = imgLoader.height;
}
imgLoader.src = imgSrc;

注意:要把imgLoader挂载src的地方放在onload以后,以防止有缓存的情况下onload事件不执行。

  • 问题2:如何计算图片展示的起点及图片应该展示的宽度?
  • 答:在此类情况下,可以直接定位到中间部分。
<style>
    .img_box{
        width: 300px;
        height: 200px;
        background: black;
        position: relative;
        overflow: hidden;
    }
    .img{
        position: absolute;
    }
</style>
<script>
    let imgLoader = new Image();
    let img = document.querySelector('.img'), box = document.querySelector('.img_box');
    let imgSrc = './pic1.jpg';
    imgLoader.onload = () => {
        let width = imgLoader.width, height = imgLoader.height;
        if(width > height){
            img.style.height = '100%';
            img.style.left = '50%';
            img.style.transform = 'translateX(-50%)';
        }else if(width < height){
            img.style.width = '100%';
            img.style.top = '50%';
            img.style.transform = 'translateY(-50%)';
        }
        img.src = imgSrc;
    }
    imgLoader.src = imgSrc;
</script>
  • 问题3:如果图片的宽高相同且box为宽大于高该怎么展示?
  • 答:当box的宽大于高,此时不可将图片的高度设置为与box相同,否则展示的样式会与最大边展示相同,如下图所示。
    box宽度大于高度
    将img高度设置为与box高度相同
    因此,此种情况,图片的高度应自适应,宽度与box同宽,即该类情况与图片的宽度小于图片的高度相同。
let imgLoader = new Image();
let img = document.querySelector('.img'), box = document.querySelector('.img_box');
let imgSrc = './pic1.jpg', style = 'cover';
imgLoader.onload = () => {
    let width = imgLoader.width, height = imgLoader.height;
    if(width > height || (width == height && box.offsetWidth < box.offsetHeight)){
        img.style.height = '100%';
        img.style.left = '50%';
        img.style.transform = 'translateX(-50%)';
    }else if(width < height || (width == height && box.offsetWidth > box.offsetHeight)){
        img.style.width = '100%';
        img.style.top = '50%';
        img.style.transform = 'translateY(-50%)';
    }
    img.src = imgSrc;
}
imgLoader.src = imgSrc;

同样的道理,如果以最大边作为展示的依据,则把两个规则调转一下即可,并增加一个选项。

    let imgLoader = new Image();
    let img = document.querySelector('.img'),box = document.querySelector('.img_box');
    let imgSrc = './pic2.jpg',style = 'cover';
    imgLoader.onload = () => {
        let width = imgLoader.width, height = imgLoader.height;
        if(style == 'cover'){
            if(width > height || (width == height && box.offsetWidth < box.offsetHeight)){
                img.style.height = '100%';
                img.style.left = '50%';
                img.style.transform = 'translateX(-50%)';
            }else if(width < height || (width == height && box.offsetWidth > box.offsetHeight)){
                img.style.width = '100%';
                img.style.top = '50%';
                img.style.transform = 'translateY(-50%)';
            }else{
                img.style.width = '100%';
                img.style.height = '100%';
            }
        }else if(style == 'contain'){
            if(width > height || (width == height && box.offsetWidth < box.offsetHeight)){
                img.style.width = '100%';
                img.style.top = '50%';
                img.style.transform = 'translateY(-50%)';
            }else if(width < height || (width == height && box.offsetWidth > box.offsetHeight)){
                img.style.height = '100%';
                img.style.left = '50%';
                img.style.transform = 'translateX(-50%)';
            }else{
                img.style.width = '100%';
                img.style.height = '100%';
            }
        }
        img.src = imgSrc;
    } 
    imgLoader.src = imgSrc;

这里按照background-size的方法,增加style参数,如果style='cover',表示按最小边展示,如果style='cover',表示按最大边展示。

  • 问题4:如果按照最小边展示时,同比例压缩后的图片的宽小于box的宽怎么办?
  • 答:有一种特殊情况,图片的宽大于高,但是同比例压缩后,图片的宽度仍然小于box的宽度,如下图所示。
    异常情况
    异常情况展示
    因此,这里我们不应该使用图片的宽与高大小的差别来判断如何展示,而应该通过对图片与box的宽高比进行展示的判断。
    let imgLoader = new Image();
    let img = document.querySelector('.img'),box = document.querySelector('.img_box');
    let imgSrc = './pic2.jpg',style = 'cover';
    imgLoader.onload = () => {
        let width = imgLoader.width, height = imgLoader.height;
        if(style == 'cover'){
            if(width/height > box.offsetWidth/box.offsetHeight){
                img.style.height = '100%';
                img.style.left = '50%';
                img.style.transform = 'translateX(-50%)';
            }else if(width/height < box.offsetWidth/box.offsetHeight){
                img.style.width = '100%';
                img.style.top = '50%';
                img.style.transform = 'translateY(-50%)';
            }else{
                img.style.width = '100%';
                img.style.height = '100%';
            }
        }else if(style == 'contain'){
            if(width/height > box.offsetWidth/box.offsetHeight){
                img.style.width = '100%';
                img.style.top = '50%';
                img.style.transform = 'translateY(-50%)';
            }else if(width/height < box.offsetWidth/box.offsetHeight){
                img.style.height = '100%';
                img.style.left = '50%';
                img.style.transform = 'translateX(-50%)';
            }else{
                img.style.width = '100%';
                img.style.height = '100%';
            }
        }
        img.src = imgSrc;
    } 
    imgLoader.src = imgSrc;

img宽>img高

原图
方法一宽>高cover
方法一宽>高contain
方法一宽<高cover
方法一宽<高contain
img宽>img高
原图
方法一宽>高cover
方法一宽>高contain
方法一宽<高cover
方法一宽<高contain
img宽=img高
原图
方法一宽>高cover
方法一宽>高contain
方法一宽<高cover
方法一宽<高contain
上面的一些图片,左侧是用方法一做到的效果,而右侧是用background-size:cover/contain达到的效果,可以看出,完全一致。

优化尺寸获取方式

但是上面的方法本质上是在加载过一次图片后,获取了宽高再进行调整。

虽然通常情况下,浏览器的资源缓存机制会使图像会只加载一次并在整个会话中使用,但是new Image()这种方法通常用作预加载,在此处我们不需要进行预加载,因此我们可以做这样的修改。

let img = box.getElementsByTagName('img')[0];
img.onload = () =>{
    let width = img.offsetWidth, height = img.offsetHeight;
    if(type == 'cover'){
        if(width > height || (width == height && box.offsetWidth < box.offsetHeight)){
            img.style.height = '100%';
            img.style.left = '50%';
            img.style.transform = 'translateX(-50%)';
        }else if(width < height || (width == height && box.offsetWidth > box.offsetHeight)){
            img.style.width = '100%';
            img.style.top = '50%';
            img.style.transform = 'translateY(-50%)';
        }else{
            img.style.width = '100%';
            img.style.height = '100%';
        }
    }else if(type == 'contain'){
        if(width > height || (width == height && box.offsetWidth < box.offsetHeight)){
            img.style.width = '100%';
            img.style.top = '50%';
            img.style.transform = 'translateY(-50%)';
        }else if(width < height || (width == height && box.offsetWidth > box.offsetHeight)){
            img.style.height = '100%';
            img.style.left = '50%';
            img.style.transform = 'translateX(-50%)';
        }else{
            img.style.width = '100%';
            img.style.height = '100%';
        }
    }
}

上面这种方式通过直接加载图片,然后获取图片的尺寸,再根据尺寸动态的进行修改图片的 展示。同样,也可以创建一次实例然后利用canvas加载图像的方法来展示。

let canvas =document.getElementById("canvas"),width = 300,height = 180,imgSrc = './pic2.jpg',style = 'contain';
// 设置宽高通过js来控制,不要用css
canvas.width = width;
canvas.height = height;
let ctx = canvas.getContext("2d");
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, width, height);
let img = new Image();
img.onload = ()=>{ 
    let {startX, startY, lengthWidth, lengthHeight} = setSize(style,width,height,img.width,img.height);
    ctx.drawImage(img, startX, startY, lengthWidth, lengthHeight, 0, 0, width, height); 
}
img.src = imgSrc;
/*
 * 设置图片的展示方式
 * @param {Number} style 确定取最小边还是最长边, box_width 固定盒子的宽, box_height 固定盒子的高
 * @param {Number} source_width 原图片的宽, source_height 原图片的高
 * @return {Object} {截取的图片信息},对应drawImage(imageResource, startX, startY, lengthWidth, lengthHeight, dx, dy, dWidth, dHeight)参数
 */
 function setSize(style,box_width, box_height, source_width, source_height){
    let startX = 0,startY = 0,lengthWidth = source_width,lengthHeight = source_height;
    if(style == 'cover'){
        if(source_width > source_height || (source_width == source_height && box_width < box_height)){
            lengthWidth = box_width*lengthHeight/box_height;
            startX = (source_width - lengthWidth)/2;
        }else if(source_width < source_height || (source_width == source_height && box_width > box_height)){
            lengthHeight = box_height*lengthWidth/box_width;
            startY = (source_height-lengthHeight)/2;
        }else{
            lengthWidth = box_width;
            lengthWidth = box_height;
        }
    }else if(style == "contain"){
        if(source_width > source_height || (source_width == source_height && box_width < box_height)){
            lengthHeight = box_height*lengthWidth/box_width;
            startY = (source_height-lengthHeight)/2;
        }else if(source_width < source_height || (source_width == source_height && box_width > box_height)){
            lengthWidth = box_width*lengthHeight/box_height;
            startX = (source_width - lengthWidth)/2;
        }else{
            lengthWidth = box_width;
            lengthWidth = box_height;
        }
    }
    return {startX,startY,lengthWidth,lengthHeight}
}

在写canvas时请注意,canvas的宽高请使用js来调整,而不要修改css。

简单理解,canvas原理就是一个画画的原理,canvas这个标签是画板,而在canvas中展示的东西是在画布上的。在渲染canvas时,初始化画板为300*150,同样画布也是300*150。当通过改变canvas的style进行宽高的修改时,其实仅仅是修改画板的宽高,而当画板与画纸尺寸不统一时,画纸内的图像就会趋向于与画板的尺寸相同,必然会导致图像的拉伸或者压缩。

这里可以参考该文章

因此,在修改尺寸的时候,要同步修改画板与画纸,这里可以直接使用用HTML 5 canvas 的api动态控制。

总结

以上三种方法,都可以得到与background-size:cover;background-position:center;object-fit:cover;object-position:center;相同的效果。各位也可以根据自己的需要调整max-heighttransform等属性。

这里提供下demo,地址可见:github.com/tingchow/se…

如有问题,欢迎提出修改。