放大镜效果

607 阅读17分钟

这里的放大镜效果是指当鼠标(手指)和页面(屏幕)中一个方框内的图片交互时,图片呈现放大效果,图片在框内显示的放大区域随鼠标(页面)位置的改变而改变

zoomOutEffect.gif

演示链接:codesandbox.io/s/fangdajin…

以下我尝试了几种不同的实现方式

移动图片框内的图片

这里是通过改变图片框内的一张图片的大小和绝对定位偏移量实现最终效果

HTML的基本结构如下:

<div id="image">
    <!-- 该图片设置为绝对定位 -->
    <img src="./img/doge.webp" alt="doge">
</div>

这里JS中涉及到的事件有mouseentermouseleavemousemove,这些事件都在图片框绑定(#image

mouseenter事件处理函数中,只需要将框中的图片尺寸放大即可:

let imageBox = document.getElementById('image');

imageBox.addEventListener('mouseenter', () => {
    // 这里将图片(图片和框一样为正方形)放大为 450x450 ,相当于图片框的3倍(150x150)
    img.style.width = '450px';
})

mouseleave事件处理函数中则只需要将定位的偏移样式和图片的放大尺寸样式清除即可:

// 清除所有样式 >> 恢复原样
imageBox.addEventListener('mouseleave', () => {
    img.style.width = '';
    img.style.top = '';
    img.style.left = '';
})

图片的放大效果的实现重点是mousemove的事件处理函数中关于图片定位偏移量的计算,这里的思路如下:

先找出当前鼠标位置相对于图片框左上角的坐标位置x, y,接着计算出这个坐标位置在图片框中的相对百分比位置(比如该点在框的正中间的话就是0.5, 0.5或者50%, 50%,这个百分比是相对于框体的左上角而言的),用这个百分比位置在放大的图片中找到相对于放大的图片左上角相同百分比偏移的位置targetX, targetY,然后x, ytargetX, targetY这两个点通过定位偏移重叠即可。为了将这两个点重叠,要求出这两个点的水平距离和垂直距离distanceX, distanceY,由于需要将图片相对于图片框的进行绝对定位偏移,那么就需要进行反向偏移,则需要对求出的distanceX, distanceY取负值,也就是最终的定位偏移为left: - distanceX + 'px'; top: - distanceY + 'px'

以下为具体计算步骤:

  • 计算当前鼠标位置相对于图片框左上角的坐标x, y

    鼠标在页面中的具体位置可以通过e.pageX, e.pageY获得(e表示鼠标事件对象),这个具体位置是相对于页面的左上角计算的,那么相对于框体的左上角位置,可以通过减去框体的顶边和左边距离页面的顶边和左边的距离(也就是imageBox.offsetTop, imageBox.offsetLeft)获得:

    x = e.pageX - imageBox.offsetLeft;
    y = e.pageY - imageBox.offsetTop;
    
  • 将坐标转化成在图片框中的相对百分比位置:

    相对于整个框体的相对位置的计算需要使用到框体的高度和宽度(imageBox.offsetWidth, imageBox.offsetHeight

    x / imageBox.offsetWidth;
    y / imageBox.offsetHeight;
    
  • 计算放大的图片中相同相对位置下的目标点targetX, targetY:

    这需要利用到上述计算得到的相对位置,再去乘以放大的图片尺寸(img.offsetWidth, img.offsetHeight)获得

    targetX = img.offsetWidth * (x / imageBox.offsetWidth);
    targetY = img.offsetHeight * (y / imageBox.offsetHeight);
    
  • 计算需要进行偏移的距离distanceX, distanceY:

    偏移的水平距离和垂直距离就是上述两个相对位置x, ytargetX, targetY的水平距离和垂直距离的差值:

    distanceX = targetX - x;
    distanceY = targetY - y;
    
  • 将图片相对于图片框进行绝对定位偏移,需要将图片反向移动,所以需要对偏移距离的值取负(设置样式要加上单位“px”):

    img.style.left = - distanceX + 'px';
    img.style.top = - distanceY + 'px';
    

至此对于图片定位偏移量的计算就结束了(具体计算可以参见下图解析)

imgMove.png

所以最终的mousemove的事件处理函数为:

imageBox.addEventListener('mousemove', (e) => {
    // 当前鼠标位置相对于图片框左上角的坐标x, y
    let x = e.pageX - imageBox.offsetLeft;
    let y = e.pageY - imageBox.offsetTop;
    // 放大的图片中相同相对位置下的目标点targetX, targetY
    let targetX = img.offsetWidth * x / imageBox.offsetWidth;
    let targetY = img.offsetHeight * y / imageBox.offsetHeight;
    // 需要进行偏移的距离distanceX, distanceY
    let distanceX = targetX - x;
    let distanceY = targetY - y;
    // 将图片相对于图片框进行绝对定位偏移,需要将图片反向移动,所以需要对偏移距离的值取负
    img.style.left = - distanceX + 'px';
    img.style.top = - distanceY + 'px';
})

移动图片框的背景图片

这里介绍第二种方式:将上述的图片框内的图片移除,直接将要放大的图片作为图片框的背景图片,通过改变背景图片大小和背景图片位置来实现最终效果

HTML的基本结构如下:

<div id="image"></div>

这里涉及到的三个JS事件还是mouseentermouseleavemousemove,这些事件都和图片框绑定(imageBox

mouseenter事件处理函数中,只需要将框的背景图片尺寸放大即可:

image.addEventListener('mouseenter', (e) => {
    // 为了提高事件处理函数的通用性,最好就不要引用外部变量 image
    // image.style.backgroundSize = '450px';
    e.target.style.backgroundSize = '450px';
});

mouseleave事件处理函数中则是只需要将背景图片的尺寸和位置样式清除即可:

image.addEventListener('mouseleave', (e) => {
    // image.style.backgroundSize = '';
    // image.style.backgroundPosition = '';
    e.target.style.backgroundSize = '';
    e.target.style.backgroundPosition = '';
});

这里的实现重点依然是对图片偏移量的计算,不过不一样的是,这里的背景图片尺寸信息比较难找到相关属性值来动态获取(如elem.offsetWidth),不能像上述采用绝对定位的做法那样求出具体的偏移距离。而如果设为定值,那么当背景图片的放大尺寸变了,程序要改的地方就比较多了

这里利用背景图片位置的百分比取值来实现将图片移动到目标位置的目的(这样甚至都不用计算具体偏移距离了!)

这里简单讲一下背景图片位置background-position取值为百分数时的表现:

百分比取值执行的是下述公式,最终的结果是使容器和背景图片相同的相对位置重叠

(container width - image width) * (position x%) = (x offset value)
(container height - image height) * (position y%) = (y offset value)

从上述公式可以看出当背景图片大小和容器大小相同时,使用百分比取值是不会有偏移的

其实百分比取值的表现说白了就是指定一个相对于图片左上角的一个相对位置,这个位置会和容器相对于左上角的相同的相对位置重叠,比如background-position: 50% 50%,这个点指定的是背景图片的正中心,那么此时该取值会使背景图片的正中心和容器的正中心重合

回归正题,此时采用背景图片位置的百分比取值,则只需要计算出当前鼠标位置相对于图片框左上角的坐标x, y在图片框中的相对百分比位置,就可以实现背景图片的正确偏移

relativePos.png

image.addEventListener('mousemove', (e) => {
    // let x = e.pageX - image.offsetLeft;
    // let y = e.pageY - image.offsetTop;
    // // 偏移百分比
    // let xOffset = x / image.offsetWidth * 100;
    // let yOffset = y / image.offsetHeight * 100;

    // image.style.backgroundPosition = xOffset + '% ' + yOffset + '%';

    // 计算出鼠标当前相对于图片框左上角的坐标
    let x = e.pageX - e.target.offsetLeft;
    let y = e.pageY - e.target.offsetTop;

    // 计算出相对位置
    // 乘 100 是为了转成百分数的大小
    let xOffset = x / e.target.offsetWidth * 100;
    let yOffset = y / e.target.offsetHeight * 100;

    e.target.style.backgroundPosition = xOffset + '% ' + yOffset + '%';
});

移动图片框的背景图片——使用尽可能多的CSS特性

这里主要是我看了B站的一个视频教程后的一个尝试,虽然其中的步骤略显繁杂,但其实现的过程让我学习到了新的知识

这里的实现使用了尽可能多的CSS特性,另外还尝试了未曾用过的API(getBoundingClientRect()e.offsetXe.offsetY

这里依然使用移动背景图片的方式,涉及到的JS事件还是mouseentermouseleavemousemove(都和图片框绑定)

mouseenter中放大图片的操作不再是直接放大背景图片,而是通过添加属性的方式间接实现

这里为图片框设置一个zoomed="1"的属性,来表示此时处于放大状态,这样的话需要配合 CSS 的属性选择器来改变背景图片尺寸:

<div id="image"></div>
#image[zoomed] {
    background-size: 450px auto;
}
image.addEventListener('mouseenter', (e) => {
    e.target.setAttribute('zoomed', 1);
})

当鼠标离开时需要图片缩小,那么只需要移除属性即可:

image.addEventListener('mouseleave', (e) => {
    e.target.removeAttribute('zoomed');
});

zoomedAttr.gif

接下来在mousemove事件处理函数中还是通过改变背景图片位置的百分比取值实现图片位置的偏移,但这里不直接改变背景图片位置的样式,而是通过改变两个CSS属性(--x--y)来间接改变图片位置样式。并且图片框尺寸是通过getBoundingClientRect()返回的一个对象的widthheight属性获取。另外当前鼠标位置相对于框体左上角的坐标位置不再需要特地进行计算获得,而是直接使用e.offsetXe.offsetY,这两个属性表示的就是相对于框体左上角计算的鼠标位置

image.addEventListener('mousemove', moveHandler(e) {
    // 这里特地尝试使用了 getBoundingClientRect()
    // 这里不作解释,只要知道它可以方法返回元素的大小及其相对于视口的位置即可
    let rect = e.target.getBoundingClientRect();

    // 这里的 e.offsetX 和 e.offsetY 表示的是鼠标相对于框体左上角的位置,相当于上述特地计算的 x 和 y
    // rect.width 相当于 e.target.offsetWidth ,rect.height 相当于 e.target.offsetHeight
    let xOffset = e.offsetX / rect.width;
    let yOffset = e.offsetY / rect.height;

    // 通过改变两个CSS属性(--x和--y)来间接改变图片位置样式
    e.target.style.setProperty('--x', xOffset);
    e.target.style.setProperty('--y', yOffset);
});

上述事件处理函数中最后设置了两个 CSS 变量的值,这时应该使用起来,使其生效:

#image[zoomed] {
    background-size: 450px auto;
    /* 将 --x 和 --y 通过 var() 解析出计算值并应用到背景图片位置样式中 */
    background-position: calc(var(--x) * 100%) calc(var(--y) * 100%);
}

尝试兼容移动端

上述的应用都是在PC端下的,是无法在移动端使用的,因为移动端下并没有鼠标事件,所以上述的事件都不会触发,自然的,放大镜的效果就无法实现了

在移动端下需要使用触摸事件来实现这个效果,这里涉及到的触摸事件有touchstarttouchendtouchmove,这些事件都和图片框绑定

在具体实现上的思路和上述一致:触摸开始(touchstart)时图片放大,触摸结束(touchend)时图片恢复原来大小并清除图片偏移样式,触摸点移动时(touchmove)需要计算图片的偏移。只是在具体实现上需要进行一些编码调整

这里采用移动背景图片的方式,以下是对上面的那个使用了尽可能多的 CSS 属性的做法进行移动端兼容(使用移动图片框内图片的方式大同小异,只是在计算偏移量方面的计算量会大一点)

touchstarttouchend的事件处理函数的内容可以和上述mouseentermouseleave的分别一样:

image.addEventListener('touchstart', (e) => {
    e.target.setAttribute('zoomed', 1);
});

image.addEventListener('touchend', (e) => {
    e.target.removeAttribute('zoomed');
});

这里差异较大的地方在于touchmove的事件处理函数内容。

  • 由于触发触摸事件的触摸点可能不止一个(多个手指触摸屏幕),所以这里在进行计算时需要特别的指明一个触摸点,使用e.touches[0]而不是e指定一个触摸事件对象,避免出现混乱,触摸事件源可以通过e.touches[0].target获得

  • 由于触摸事件对象并没有e.offsetXe.offsetY这两个属性,所以触摸点位置相对于框体左上角的坐标依然需要特地进行计算

    这里由于我想特地去尝试getBoundingClientRect(),在计算过程中为了获取框体的位置信息,需要使用该方法返回的对象的topleft属性获得,并且要注意:该方法返回的框体位置信息是相对于视口而不是相对于页面而言的,所以如果页面发生滚动时,使用相对于页面计算的触摸点位置属性e.pageXe.pageY计算出的结果是不准确,这里应该采用同样相对于视口计算的触摸点位置属性e.clientXe.clientY

    // 使用相对于视口计算的触摸点位置 clientX 和 clientY
    // rect = e.target.getBoundingClientRect()
    x = e.touches[0].clientX - rect.left;
    y = e.touches[0].clientY - rect.top;
    
    // 如果要使用相对于页面计算的触摸点位置 pageX 和 pageY,则需要采用同样相对于页面计算的框体位置信息 offsetLeft 和 offsetTop
    x = e.touches[0].pageX - e.touches[0].target.offsetLeft;
    y = e.touches[0].pageY - e.touches[0].target.offsetTop;
    

    这样就可以计算出相对位置的信息了:

    // rect = e.target.getBoundingClientRect()
    xOffset = x / rect.width;
    yOffset = y / rect.height;
    
    // 如果使用相对于页面计算的触摸点位置,则需要这样计算相对位置
    // xOffset = x / e.touches[0].target.offsetWidth;
    // yOffset = y / e.touches[0].target.offsetHeight;
    

由于在对屏幕进行触摸移动时,默认会拖动页面,为了避免这种情况,需要使用e.preventDefault()

另外由于一个触摸点长时间接触屏幕而不移动时,会触发页面菜单,需要通过contextmenu事件来禁止长按触发菜单

mobileDragTrigMenu.gif

<div id="image"></div>
// 禁止长按菜单
document.addEventListener('contextmenu', (e) => {
    e.preventDefault();
})
image.addEventListener('touchmove', (e) => {
    // 防止页面被拖动
    e.preventDefault();

    let rect = e.target.getBoundingClientRect();

    // 由于 touch 事件没有 offsetX 和 offsetY 属性,所以需要进行额外的计算操作
    // 可能有多个触摸点,这里只取一个触摸点来计算触摸的位置 e.touches[0]
    // 使用相对于视口计算的触摸点位置 clientX 和 clientY(方法返回的框体位置信息是相对于视口而言的)
    // rect = e.target.getBoundingClientRect()
    let x = e.touches[0].clientX - rect.left;
    let y = e.touches[0].clientY - rect.top;

    // 如果要使用相对于页面计算的触摸点位置 pageX 和 pageY,则需要采用同样相对于页面计算的框体位置信息 offsetLeft 和 offsetTop
    // let x = e.touches[0].pageX - e.touches[0].target.offsetLeft;
    // let y = e.touches[0].pageY - e.touches[0].target.offsetTop;

    let xOffset = x / rect.width;
    let yOffset = y / rect.height;

    // 如果使用相对于页面计算的触摸点位置,则需要这样计算相对位置
    // let xOffset = x / e.touches[0].target.offsetWidth;
    // let yOffset = y / e.touches[0].target.offsetHeight;

    e.target.style.setProperty('--x', xOffset);
    e.target.style.setProperty('--y', yOffset);
});

此时有个问题就是当触摸点触摸屏幕但不移动时,图片显示的放大区域可能是不正确的(如下,即使触摸的位置是其他位置,也总是显示图片左上角的那只耳朵)

wrongZoomedPos.gif

这是因为此时触摸点不移动,就不会触发touchmove事件,导致无法对图片进行移动,所以这里需要在touchstart事件处理函数中添加相关步骤来初始移动图片

// 这里将触摸移动事件处理函数体抽离为一个独立的函数
image.addEventListener('touchstart', (e) => {
    e.target.setAttribute('zoomed', 1);
    // 避免重新触摸后触摸点未移动时图片位置不正确
    moveHandler(e);
});

image.addEventListener('touchmove', moveHandler);

function moveHandler(e) {
    // 防止页面被拖动
    e.preventDefault();

    let rect = e.target.getBoundingClientRect();

    // 由于 touch 事件没有 offsetX 和 offsetY 属性,所以需要进行额外的计算操作
    // 可能有多个触摸点,这里只取一个触摸点来计算触摸的位置 e.touches[0]
    // 使用相对于视口计算的触摸点位置 clientX 和 clientY(方法返回的框体位置信息是相对于视口而言的)
    // rect = e.target.getBoundingClientRect()
    let x = e.touches[0].clientX - rect.left;
    let y = e.touches[0].clientY - rect.top;

    // 如果要使用相对于页面计算的触摸点位置 pageX 和 pageY,则需要采用同样相对于页面计算的框体位置信息 offsetLeft 和 offsetTop
    // let x = e.touches[0].pageX - e.touches[0].target.offsetLeft;
    // let y = e.touches[0].pageY - e.touches[0].target.offsetTop;

    let xOffset = x / rect.width;
    let yOffset = y / rect.height;

    // let xOffset = x / e.touches[0].target.offsetWidth;
    // let yOffset = y / e.touches[0].target.offsetHeight;

    e.target.style.setProperty('--x', xOffset);
    e.target.style.setProperty('--y', yOffset);
}

修改后效果如下(现在触摸哪里,放大的位置就是哪里)

rightZoomedPos.gif

现在还有一个问题就是当触摸点在图片框内移动到框外区域时,touchmove事件还是处于被触发的状态,此时触摸点的位置在框外,而对于图片偏移量的计算依然根据触摸点位置计算,那么此时就会导致图片被移动到可见区域外

outOfBoxTouchmove.gif

为解决这个问题需要限制图片可偏移的范围,只需要做些简单判断即可

function moveHandler(e) {
    // ......
    
    // 使用相对于视口计算的触摸点位置 clientX 和 clientY(方法返回的框体位置信息是相对于视口而言的)
    // rect = e.target.getBoundingClientRect()
    let x = e.touches[0].clientX - rect.left;
    let y = e.touches[0].clientY - rect.top;
    // 限制范围
    x = x < 0 ? 0 : x;
    x = x > rect.width ? rect.width : x;
    y = y < 0 ? 0 : y;
    y = y > rect.height ? rect.height : y;

    // 如果要使用相对于页面计算的触摸点位置 pageX 和 pageY,则需要采用同样相对于页面计算的框体位置信息 offsetLeft 和 offsetTop
    // let x = e.touches[0].pageX - e.touches[0].target.offsetLeft;
    // let y = e.touches[0].pageY - e.touches[0].target.offsetTop;
    // 限制范围
    // x = x < 0 ? 0 : x;
    // x = x > e.touches[0].target.offsetWidth ? e.touches[0].target.offsetWidth : x;
    // y = y < 0 ? 0 : y;
    // y = y > e.touches[0].target.offsetHeight ? e.touches[0].target.offsetHeight : y;

    // ......
}

为了让代码看起来NB一点,可以将上述的判断语句合并一下:

function moveHandler(e) {
    // ......
    
    // 限制范围
    x = Math.min(Math.max(0, x), rect.width);
    y = Math.min(Math.max(0, y), rect.height);
    // x = x < 0 ? 0 : x;
    // x = x > rect.width ? rect.width : x;
    // y = y < 0 ? 0 : y;
    // y = y > rect.height ? rect.height : y;

    // ......
    // 限制范围
    // x = Math.min(Math.max(0, x), e.touches[0].target.offsetWidth);
    // y = Math.min(Math.max(0, y), e.touches[0].target.offsetHeight);
    // x = x < 0 ? 0 : x;
    // x = x > e.touches[0].target.offsetWidth ? e.touches[0].target.offsetWidth : x;
    // y = y < 0 ? 0 : y;
    // y = y > e.touches[0].target.offsetHeight ? e.touches[0].target.offsetHeight : y;

    // ......
}

至此,触摸点即使移动出了图片框外也可以正确显示放大的图片了

outOfBoxTouchmoveOK.gif

至此移动端的放大镜效果实现就完成了,现在需要将移动端的相关代码和PC端的相关代码合并达成兼容的目的

为了方便为鼠标事件和触摸事件赋予事件处理函数,同时使代码结构看起来优雅一点,这里我将事件处理函数的代码进行抽离。由于mouseentermouseleave的事件处理函数分别和touchstarttouchend的内容几乎一致,所以这四个事件的处理函数并不需要做太多的差异编码。这里有一个禁止长按菜单的操作(contextMenu事件)是只需要在移动端内设置的,所以需要判断此时触发的事件是移动端事件还是PC端的,以此来决定是否要禁止长按触发菜单

image.addEventListener('mouseenter', enterHandler);
image.addEventListener('mouseleave', leaveHandler);

image.addEventListener('touchstart', enterHandler);
image.addEventListener('touchend', leaveHandler);

// 这里需要特地抽离出一个 contextmenu 事件处理函数,因为 removeEventListener 无法移除一个为匿名函数的监听器
function prevent(e) {
    e.preventDefault();
}

function enterHandler(e) {
    e.target.setAttribute('zoomed', 1);
    // 避免重新触摸后未移动时图片位置不正确
    moveHandler(e);

    if (e.type === 'touchend') {
        // 禁止长按菜单
        document.addEventListener('contextmenu', prevent)
    } else {
        // 取消禁止长按菜单
        document.removeEventListener('contextmenu', prevent);
    }
}

function leaveHandler(e) {
    e.target.removeAttribute('zoomed');
}

mousemovetouchmove事件的事件处理函数内容差异较大,需要进行的差异编码比较多,其中重要的一个就是对事件类型的判断(详见以下代码)

image.addEventListener('mousemove', moveHandler);
image.addEventListener('touchmove', moveHandler);

function moveHandler(e) {
    let rect = e.target.getBoundingClientRect();

    // 由于 touch 事件没有 offsetX 和 offsetY 属性,所以需要进行额外的计算操作
    let x = 0;
    let y = 0;

    // 对事件类型的判断
    if (['touchstart', 'touchend', 'touchmove'].includes(e.type)) {
        // 防止页面被拖动
        e.preventDefault();
        
        // 可能有多个触摸点,这里只取一个触摸点来计算触摸的位置 e.touches[0]
        // 使用相对于视口计算的触摸点位置 clientX 和 clientY(方法返回的框体位置信息是相对于视口而言的)
        // rect = e.target.getBoundingClientRect()
        x = e.touches[0].clientX - rect.left;
        y = e.touches[0].clientY - rect.top;
        // 限制范围
        x = Math.min(Math.max(0, x), rect.width);
        y = Math.min(Math.max(0, y), rect.height);
        // x = x < 0 ? 0 : x;
        // x = x > rect.width ? rect.width : x;
        // y = y < 0 ? 0 : y;
        // y = y > rect.height ? rect.height : y;
    } else {
        x = e.offsetX;
        y = e.offsetY;
    }

    let xOffset = x / rect.width;
    let yOffset = y / rect.height;

    e.target.style.setProperty('--x', xOffset);
    e.target.style.setProperty('--y', yOffset);
}

至此这个兼容就完成了

精简代码

可以看到,在我尝试使用getBoundingClientRect()以及e.offsetX, e.offsetY时,兼容PC端和移动端时,代码量有点多,实际上可以改变为统一做法,精简代码

image.addEventListener('mousemove', moveHandler);
image.addEventListener('touchmove', moveHandler);s

function moveHandler(e) {
    let eventObj = null;

    if (['touchstart', 'touchend', 'touchmove'].includes(e.type)) {
        // 防止页面被拖动
        e.preventDefault();

        // 可能有多个触摸点,这里只取一个触摸点来计算触摸的位置 e.touches[0]
        eventObj = e.touches[0];
    } else {
        eventObj = e;
    }

    let x = eventObj.pageX - eventObj.target.offsetLeft;
    let y = eventObj.pageY - eventObj.target.offsetTop;

    // 限制范围
    x = Math.min(Math.max(0, x), eventObj.target.offsetWidth);
    y = Math.min(Math.max(0, y), eventObj.target.offsetHeight);

    let xOffset = x / eventObj.target.offsetWidth;
    let yOffset = y / eventObj.target.offsetHeight;

    e.target.style.setProperty('--x', xOffset);
    e.target.style.setProperty('--y', yOffset);
}

总结

放大镜效果涉及到的事件在PC端是mouseentermouseleavemousemove,在移动端是touchstarttouchendtouchmove

实现的方式有不止一种,但总的来说就三个步骤:进入图片框时放大图片;离开图片框时恢复图片大小并将图片偏移样式清除;在图片框内移动鼠标或手指时图片的移动随鼠标或接触点的位置进行变化,要保证可视区域显示的图片放大位置为 鼠标或接触点在图片框相对位置 和 放大的图片相同相对位置 重叠时的图片部分

文章源码链接:gitee.com/thisismyadd…

参考:

www.bilibili.com/video/BV12N… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/…