Skip to content

卷帘效果

上次更新 2025年8月20日星期三 3:16:20 字数 0 字 时长 0 分钟

卷帘效果是一种地图可视化技术,可以通过滑动分割线来对比两个不同的地图图层。这种效果常用于对比同一区域在不同时期或不同类型的地图数据。

示例

新标签页预览

代码实现

vue
<template>
  <div class="relative h-96">
    <input
      id="swipe"
      type="range"
      class="w-full absolute top-1/2 z-1 opacity-0 cursor-ew-resize pointer-events-none"
    />
    <div class="swipe-line">
      <div class="swipe-handle"></div>
    </div>
    <div id="map" class="w-full h-full"></div>
  </div>
</template>

<script setup>
import { onMounted } from "vue";
import "ol/ol.css";
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
import XYZ from "ol/source/XYZ";
import { fromLonLat } from "ol/proj";
import { getRenderPixel } from "ol/render";

onMounted(() => {
  // 创建左侧图层(矢量图)
  const leftLayer = new TileLayer({
    source: new XYZ({
      url: "http://wprd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=7",
    }),
  });

  // 创建右侧图层(影像图)
  const rightLayer = new TileLayer({
    source: new XYZ({
      url: "http://wprd01.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scl=1&style=6",
    }),
  });

  // 创建地图
  const map = new Map({
    target: "map",
    layers: [leftLayer, rightLayer],
    view: new View({
      center: fromLonLat([116.397428, 39.90923]), // 北京
      zoom: 12,
    }),
  });

  // 获取滑块元素
  const swipe = document.getElementById("swipe");
  const swipeLine = document.querySelector(".swipe-line");
  const swipeHandle = document.querySelector(".swipe-handle");

  // 监听滑块手柄拖动
  let isDragging = false;
  let startX = 0;
  let startLeft = 50;

  swipeHandle.addEventListener("mousedown", function (e) {
    isDragging = true;
    startX = e.clientX;
    startLeft = parseInt(swipeLine.style.left || 50);
    e.preventDefault();
  });

  document.addEventListener("mousemove", function (e) {
    if (!isDragging) return;

    const dx = e.clientX - startX;
    const containerWidth = map.getTargetElement().offsetWidth;
    let newLeft = startLeft + (dx / containerWidth) * 100;

    // 限制范围在0-100之间
    newLeft = Math.max(0, Math.min(100, newLeft));

    swipeLine.style.left = newLeft + "%";
    swipe.value = newLeft;
    map.render();
  });

  document.addEventListener("mouseup", function () {
    isDragging = false;
  });

  // 渲染前处理
  rightLayer.on("prerender", function (event) {
    const ctx = event.context;
    const mapSize = map.getSize();
    const width = mapSize[0] * (swipe.value / 100);

    const tl = getRenderPixel(event, [width, 0]);
    const tr = getRenderPixel(event, [mapSize[0], 0]);
    const bl = getRenderPixel(event, [width, mapSize[1]]);
    const br = getRenderPixel(event, mapSize);

    ctx.save();
    ctx.beginPath();
    ctx.moveTo(tl[0], tl[1]);
    ctx.lineTo(bl[0], bl[1]);
    ctx.lineTo(br[0], br[1]);
    ctx.lineTo(tr[0], tr[1]);
    ctx.closePath();
    ctx.clip();
  });

  // 渲染后处理
  rightLayer.on("postrender", function (event) {
    const ctx = event.context;
    ctx.restore();
  });
});
</script>

<style scoped>
.h-96 {
  height: 24rem;
}

.relative {
  position: relative;
}

.swipe-line {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 4px;
  background: #fff;
  z-index: 2;
  pointer-events: none;
  left: 50%;
  transform: translateX(-50%);
}

.swipe-handle {
  position: absolute;
  width: 24px;
  height: 24px;
  background: #fff;
  border-radius: 50%;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
  pointer-events: all;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  cursor: ew-resize;
}

.swipe-handle::before,
.swipe-handle::after {
  content: "";
  position: absolute;
  width: 2px;
  height: 10px;
  background: #666;
  left: 7px;
  top: 7px;
}

.swipe-handle::after {
  left: 15px;
}
</style>
关注公众号