前言

平常,我们在查看图片的时候,都有放大缩小的功能。如下图👇

JavaScript 图片缩放_缩放

那么,我们如何在网页中,对图像进行缩放呢?

本文,我们来讲讲如何使用 JavaScript 实现图片的缩放。当然,我们可以类比到其他的元素,比如视频的缩放。

更改宽度

是的,很符合第一直觉逻辑的一种实现方式。电脑上查看相片也是使用的这种模式 - 直接保持外侧容器的框高不变,等比例地更改图片的尺寸。

我们来简单举个例子:

<div class="container" style="width: 400px; height: 300px;">
  <img src="path/to/image.png" id="image" style="width: 400px;" />
</div>
  • 1.
  • 2.
  • 3.
(function(){
  const ratio = 4 / 3; // 宽高比例
  let imageDom = document.getElementById("image");
  imageDom.addEventListener("click", function() {
    imageDom.style.width = 400 * ratio + "px";
  })
})()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

上面代码中,我们设定了外部容器的尺寸是 400 * 300 px,内部的图像的宽度等同外部尺寸。当点击图片之后,图像的宽度变为 400 * 4 / 3 px,外部的容器没有发生更改。

那么,我们这种直接更改宽度的方法,在全屏的模式下,生效?

public static gotoFullscreen(dom: any): void {
  if (dom.requestFullscreen) {
    dom.requestFullscreen()
  } else if (dom.mozRequestFullScreen) {
    dom.mozRequestFullScreen()
  } else if (dom.webkitRequestFullscreen) {
    dom.webkitRequestFullscreen()
  } else if (dom.msRequestFullscreen) {
    dom.msRequestFullscreen()
  } else {
    console.error('当前浏览器不支持部分全屏!')
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

也就是通过上面的代码进入到浏览器的全屏模式 gotoFullscreen(document.getElementsByClassName("container")[0])

然而,无论我们怎么设定图像的宽度,比如 document.getElementById("image").style.width = "200%",都不会生效的。

我们是否还有其他进行缩放的方法在全屏模式下也能够实现呢?

更改 Scale

我们可以保持图片的实际的宽高是不变的,然后更改其 scaleXscaleY 来实现。

<div class="container" style="width: 400px; height: 300px;">
  <img src="path/to/image.png" id="image" style="width: 400px;" />
</div>
  • 1.
  • 2.
  • 3.
(function(){
  const ratio = 4 / 3; // 框高比例
  let imageDom = document.getElementById("image");
  imageDom.addEventListener("click", function() {
    imageDom.style.transform = `scale(4/3, 4/3)`;
  })
})()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

很明显,与 更改宽度 小节,唯一不同的点就是 imageDom.style.transform = scale(4/3, 4/3);,我们在点击图片的时候,使用 transform 属性值 scale(x, y) 对其 x 轴和 y 轴进行缩放。

而且,在全屏的模式下,该方法依旧能够实现对图片的缩放。因为图片的宽度不变

取舍

两种方案:更改宽度更改 Scale。我们应该选择 更改 Scale 来对图像进行缩放。因为:

  1. 更改 Scale 涉及的场景比 更改宽度 要广
  2. 更改 Scale 性能比更改宽度性能优越。因为更改宽度是对 dom 进行操作,会造成回流和重排,而更改 Scale 是利用 图形处理器(GPU) 来实现。感兴趣的读者可以看看这篇文章 从浏览器渲染原理谈动画性能优化

更改偏移位置

我们以方案二 - 更改 Scale 为基础。

当我们希望查看点击点的图片。我们需要对其进行放大,并将点击点放在外容器的中心点的位置。那么,我们就需要对图像的位置进行处理。

我们可以使用 position: absolute; top: *px; left: *px; 来实现,但是通过我们上面取舍小节的对比。我们有更好的替代方案 - 使用 translate(x, y) 来实现

这里我们使用 typescript 结合 angular 来实现:

<div id="imageContainer">
    <image
        id="image"
        [style]="{
            width: imageRealWidth,
            transform: 'scale(' + imageAmplifyMultiple + ', ' + imageAmplifyMultiple + ') translate(' + imageTranslateX + 'px, ' + imageTranslateY + 'px)'
        }"
    />
</div>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

对应的 javascript 如下:

// 放大图片区域
public amplifyImagePortion(event) {
    let imageContainerCenterLeft : number;
    let imageContainerCenterTop : number;

    let _imageContainer: any = document.getElementById('imageContainer');
    if(this.imageIsFullscreen) { // 全屏模式
        imageContainerCenterLeft = _imageContainer.getBoundingClientRect().width / 2;
        imageContainerCenterTop =_imageContainer.getBoundingClientRect().height / 2;
    } else {
        // 非全屏的模式下
        imageContainerCenterLeft = _imageContainer.getBoundingClientRect().left + _imageContainer.getBoundingClientRect().width / 2;
        imageContainerCenterTop = _imageContainer.getBoundingClientRect().top + _imageContainer.getBoundingClientRect().height / 2;
    }

    let clickPointLeft = event.pageX;
    let clickPointTop = event.pageY;

    this.imageTranslateX =  (this.imageTranslateX * this.imageAmplifyMultiple + ( imageContainerCenterLeft - clickPointLeft)) / this.imageAmplifyMultiple;
    this.imageTranslateY =  (this.imageTranslateY * this.imageAmplifyMultiple +(imageContainerCenterTop - clickPointTop)) / this.imageAmplifyMultiple;

    // 放大的倍数
    this.imageAmplifyMultiple =  this.imageAmplifyMultiple * this.multipleStep;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

上面的案例中,我们只是进行放大功能的展示。

引入鼠标滚轮

下面,我们通过引入鼠标滚动,修改下 amplifyVideoPortion 方法来对图像放大或缩小。

// 滚轮滚动
private mouseWheelFn(event) {
    if(!this.mouseWheel) {
        this.mouseWheel = fromEvent(document.getElementById('imageContainer'), 'wheel');
        this.subscriptions.push(
            this.mouseWheel
            .pipe(throttleTime(50))
            .subscribe((wheel: any) => {
                if(wheel.deltaY > 1) {
                    // 进行局部放大
                    this.amplifyImagePortion(event, true);
                }
                if(wheel.deltaY < -1) {
                    // 进行局部缩小
                    this.amplifyImagePortion(event, true, 'minify');
                }
                // 重置框选数据
                this.resetCheckBoxVariables();
            })
        );
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

我们监听外部容器选中,滚轮滚动,当正向滚动的时候,我们对图片进行局部放大,当反向滚动的时候,我们对图片进行局部缩小。我们这里还引入了 rxjs 中的节流方法 throttleTime 来优化滚轮触发事件的时机。

对应的 amplifyImagePortion 方法我们更改如下👇

// 放大图像区域
public amplifyImagePortion(event, isWheel, direction?: string) { // isWheel 是否是鼠标滚动
    let imageContainerCenterLeft : number;
    let imageContainerCenterTop : number;

    let _imageContainer: any = document.getElementById('imageContainer');
    if(this.imageIsFullscreen) { // 全屏模式下
        imageContainerCenterLeft = _imageContainer.getBoundingClientRect().width / 2;
        imageContainerCenterTop =_imageContainer.getBoundingClientRect().height / 2;
    } else {
        // 非全屏的模式下
        if(isWheel) {
            imageContainerCenterLeft = _imageContainer.getBoundingClientRect().left + _imageContainer.getBoundingClientRect().width / 2;
            imageContainerCenterTop = _imageContainer.getBoundingClientRect().top + _imageContainer.getBoundingClientRect().height / 2;
        }
    }

    let clickPointLeft: number = 0;
    let clickPointTop: number = 0;
    if(isWheel) {
        clickPointLeft = event.pageX;
        clickPointTop = event.pageY;

    } else {
        clickPointLeft = this.checkboxPositionLeft + this.checkboxWidth / 2;
        clickPointTop = this.checkboxPositionTop + this.checkboxHeight / 2
    }


    // 计算两点之间的距离
    let diffX: number = imageContainerCenterLeft - clickPointLeft;
    let diffY: number = imageContainerCenterTop - clickPointTop;


    if(!isWheel) {
        this.imageTranslateX =  (this.imageTranslateX * this.imageAmplifyMultiple + diffX) / this.imageAmplifyMultiple;
        this.imageTranslateY =  (this.imageTranslateY * this.imageAmplifyMultiple + diffY) / this.imageAmplifyMultiple;
    }

    // 缩小的倍数
    if(direction == 'minify') {
        this.imageAmplifyMultiple =  this.imageAmplifyMultiple * (1 / this.multipleStep);
    } else {
        // 放大的倍数 - 默认
        this.imageAmplifyMultiple =  this.imageAmplifyMultiple * this.multipleStep;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.

multipleStep 是放大的倍数,1 / multipleStep 是缩小的倍数。因为宽度变,我们需要对 translatex 轴和 y 轴的偏移进行合理计算,见上面两份代码。

扩展

当然,我们还可以图片缩放的功能进行扩展,比如,对图片进行区域的框选进行缩放;比如,另起一个 canvas 对图片进行绘制缩放等。