卫星遥感影像的多时相对比是变化检测中最直观的手段之一。将两期影像叠加后,通过拖动分割线让上层影像只露出一部分,就能快速观察同一区域在不同时间的地表变化。这个功能在专业GIS软件里很常见,但在Web端其实只用jQuery加上少量CSS就可以搭出一个轻量级版本。本文会从图层叠加的基本原理入手,给出可运行的滑块交互代码,并讨论影像对齐、事件处理和性能优化等细节,方便你在不引入OpenLayers或Leaflet的情况下快速验证思路。

卷帘显示的基本原理与页面结构
卷帘对比的本质是两张影像完全重叠,下层显示完整影像,上层只显示分割线一侧的内容。实现这个效果有两种常见方法:一种是动态修改上层影像的clip-path属性,把不需要显示的部分裁掉;另一种是给上层影像外面包一个宽度可变的容器,并设置overflow为hidden,让容器宽度跟随分割线变化。前者裁剪灵活,后者兼容性更好,也更适合配合jQuery的动画或滑块事件使用。
页面结构并不复杂。外层容器需要设置position为relative,两张影像都用position为absolute铺满容器,确保它们坐标原点一致。上层影像的裁剪容器也使用absolute定位,初始宽度可以设为50%。分割线可以是一个绝对定位的竖条,通过left属性控制位置。这样当用户拖动分割线时,jQuery只需要更新裁剪容器的宽度和分割线的left值,就能完成整个交互。下面给出基础结构。
<div class="swipe-container" id="swipeContainer">
<img class="swipe-image base-image" src="old-image.jpg" alt="早期影像">
<div class="swipe-clip" id="swipeClip">
<img class="swipe-image top-image" src="new-image.jpg" alt="后期影像">
</div>
<div class="swipe-divider" id="swipeDivider"></div>
</div>
这里需要注意的是,上层影像不是直接放在外层容器里,而是放在裁剪容器内部。裁剪容器需要设置overflow为hidden,宽度由jQuery控制。上层影像本身仍然使用100%的宽度和高度,这样当裁剪容器宽度变化时,上层影像不会缩放,只会被遮住一部分,从而保证两张影像像素级对齐。
CSS部分要特别处理影像的尺寸。遥感影像通常分辨率很高,但展示时会被压缩。给两张影像都设置width为100%、height为100%、object-fit为fill,可以让它们强制拉伸到容器大小。如果原图宽高比和容器不一致,fill会导致轻微变形,但对于已经做过地理配准的对比影像来说,这种拉伸比例一致,不影响相对位置。若需要保持宽高比,可以把object-fit改为cover,不过裁剪区域的计算会复杂一些。
用jQuery实现拖拽交互
卷帘工具的核心交互是拖动分割线。可以监听外层容器的mousedown事件,在鼠标按下后记录拖动状态,接着监听document的mousemove和mouseup事件来计算新的分割位置。移动端需要同时处理touchstart、touchmove和touchend。用jQuery可以让这些事件绑定更简洁,也方便统一计算坐标。
计算百分比时,先获取容器相对页面左侧的距离,再用鼠标当前位置减去容器左边界,除以容器宽度得到0到1之间的比例。这个比例乘以100就是裁剪容器的百分比宽度。分割线也需要同步移动。为了防止鼠标移出容器后事件丢失,mousemove绑定在document上而不是容器上,这样即使鼠标跑到浏览器边缘,拖拽仍能继续。下面是一个完整示例。
$(function () {
var $container = $('#swipeContainer');
var $clip = $('#swipeClip');
var $divider = $('#swipeDivider');
var dragging = false;
function setPosition(clientX) {
var rect = $container[0].getBoundingClientRect();
var offsetX = clientX - rect.left;
var percent = (offsetX / rect.width) * 100;
if (percent < 0) percent = 0;
if (percent > 100) percent = 100;
$clip.css('width', percent + '%');
$divider.css('left', percent + '%');
}
$container.on('mousedown touchstart', function (e) {
dragging = true;
e.preventDefault();
});
$(document).on('mousemove touchmove', function (e) {
if (!dragging) return;
var clientX;
if (e.type === 'touchmove' || e.type === 'touchstart') {
clientX = e.originalEvent.touches[0].clientX;
} else {
clientX = e.clientX;
}
setPosition(clientX);
});
$(document).on('mouseup touchend', function () {
dragging = false;
});
});
这段代码中,setPosition函数接收鼠标或手指的横向坐标,再通过getBoundingClientRect获取容器的实时位置。这样即使页面滚动,计算依然准确。裁剪容器宽度改成百分比后,上层影像露出的区域实时变化,分割线也同步移动。对于只需要简单切换两张影像的场景,也可以把类似逻辑绑定到range输入框,但自定义拖拽的体验更接近专业GIS软件中的卷帘工具。
如果需要让分割线支持键盘操作,可以给容器加上tabindex属性,然后监听方向键事件。按一次左右键移动1%到5%的宽度,能提升可访问性。不过遥感影像的卷帘对比更多是鼠标或触摸操作,键盘属于辅助增强。
影像对齐与加载时机
多时相遥感影像经常来自不同传感器或不同时间拍摄,分辨率、投影和影像范围可能并不一致。纯前端的卷帘工具无法做地理配准,因此必须确保两张影像在进入页面之前已经被处理为相同尺寸、相同地理范围。常见的做法是在GIS服务端或桌面端完成重采样和裁剪,输出两张像素尺寸完全相同的GeoTIFF或PNG预览图。如果直接使用未经配准的原始影像,卷帘时会出现明显错位,变化对比就失去了意义。
另一个容易忽略的问题是图片加载时机。如果上层影像还没加载完成,用户就开始拖动,裁剪容器宽度虽然正确,但内部图片可能只有一部分渲染出来。可以在两张图片的load事件之后再初始化分割位置,或者给容器一个固定宽高,避免加载过程中的布局抖动。下面示例演示如何等待图片全部加载完成。
$(function () {
var $container = $('#swipeContainer');
var $clip = $('#swipeClip');
var $divider = $('#swipeDivider');
var loadedCount = 0;
var totalImages = $container.find('img').length;
$container.find('img').on('load', function () {
loadedCount++;
if (loadedCount === totalImages) {
$clip.css('width', '50%');
$divider.css('left', '50%');
$container.addClass('ready');
}
}).each(function () {
if (this.complete) {
$(this).trigger('load');
}
});
});
这个写法会检查每张图片是否已经缓存完成,如果已经完成就手动触发load,保证初始化逻辑一定会执行。容器在未加载完成前可以设置一个统一的背景色或占位高度,避免页面高度塌陷。遥感影像的文件体积通常较大,加载等待时间要提前考虑。
性能优化与瓦片方案
如果直接加载整幅高分辨率遥感影像,单张图片可能达到几十甚至上百MB,不仅占用带宽,浏览器解码也会卡顿。对于演示或中小范围对比,可以先生成压缩后的预览图,例如把影像缩放到2000像素以内,再保存为JPEG或WebP格式。这样卷帘交互会流畅很多。真正生产环境中的Web GIS应用一般不会直接加载整幅影像,而是使用瓦片服务,按需加载当前视野范围内的切片。
引入瓦片后,卷帘对比可以在两个瓦片图层上实现。OpenLayers和Leaflet都支持多个影像图层叠加,并且可以配合裁剪操作做卷帘效果,但本文的jQuery方案更适合轻量级页面或原型验证。如果后续需要接入瓦片服务,可以把两个影像源换成地图库的图层,再用同样的分割线逻辑控制上层图层的显示范围。核心交互保持不变,只是影像来源从单张图片换成了动态切片。
拖动过程中,频繁修改DOM的宽度会触发浏览器重排。为了减少性能损耗,可以把裁剪容器的will-change属性设置为width,或者使用transform来实现裁剪。不过transform裁剪的坐标计算相对复杂,简单场景下直接改width已经足够。使用requestAnimationFrame节流也能让拖拽更平滑,避免在mousemove事件中做过多计算。下面给出一个节流版本的关键代码。
var ticking = false;
function requestPosition(clientX) {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () {
setPosition(clientX);
ticking = false;
});
}
$(document).on('mousemove touchmove', function (e) {
if (!dragging) return;
var clientX;
if (e.type === 'touchmove') {
clientX = e.originalEvent.touches[0].clientX;
} else {
clientX = e.clientX;
}
requestPosition(clientX);
});
这个版本把实际的位置更新放到下一帧渲染前执行,避免同一个帧内多次修改样式。对于高分辨率屏幕或低性能移动设备,这种优化能明显减少掉帧。需要注意的是,requestAnimationFrame回调中使用的clientX是最后一次事件的值,中间如果用户快速拖动,视觉上仍然跟手,但减少了不必要的计算。