Skip to content

图层裁剪

介绍

展开代码
vue
<template>
  <div class="map-container">
    <div ref="mapContainer" id="map"></div>
    <div class="controls">
      <div class="control-group">
        <label>裁剪形状:</label>
        <select v-model="clipShape" @change="updateClipShape">
          <option value="heart">❤️ 心形</option>
          <option value="circle">⭕ 圆形</option>
          <option value="star">⭐ 星形</option>
          <option value="rectangle">⬜ 矩形</option>
        </select>
      </div>
      <div class="control-group">
        <label>裁剪大小:</label>
        <input 
          type="range" 
          min="0.5" 
          max="3" 
          step="0.1" 
          v-model="clipSize"
          @input="updateClipSize"
        />
        <span>{{ clipSize }}x</span>
      </div>
      <div class="control-group">
        <label>
          <input type="checkbox" v-model="enableClip" @change="toggleClip" />
          启用裁剪
        </label>
      </div>
      <button @click="resetView">重置视图</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';
import 'ol/ol.css';

const mapContainer = ref(null);
let map = null;
let osmLayer = null;

// 响应式变量
const clipShape = ref('heart');
const clipSize = ref(1);
const enableClip = ref(true);

// 绘制心形路径
const drawHeart = (ctx, scale) => {
  ctx.scale(3 * scale, 3 * scale);
  ctx.translate(-75, -80);
  ctx.beginPath();
  ctx.moveTo(75, 40);
  ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
  ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
  ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
  ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
  ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
  ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
  ctx.closePath();
};

// 绘制圆形路径
const drawCircle = (ctx, scale) => {
  ctx.scale(scale, scale);
  ctx.beginPath();
  ctx.arc(0, 0, 100, 0, 2 * Math.PI);
  ctx.closePath();
};

// 绘制星形路径
const drawStar = (ctx, scale) => {
  ctx.scale(scale, scale);
  ctx.beginPath();
  const spikes = 5;
  const outerRadius = 100;
  const innerRadius = 40;
  
  for (let i = 0; i < spikes * 2; i++) {
    const angle = (i * Math.PI) / spikes;
    const radius = i % 2 === 0 ? outerRadius : innerRadius;
    const x = Math.cos(angle) * radius;
    const y = Math.sin(angle) * radius;
    
    if (i === 0) {
      ctx.moveTo(x, y);
    } else {
      ctx.lineTo(x, y);
    }
  }
  ctx.closePath();
};

// 绘制矩形路径
const drawRectangle = (ctx, scale) => {
  ctx.scale(scale, scale);
  ctx.beginPath();
  ctx.rect(-100, -80, 200, 160);
  ctx.closePath();
};

// 获取裁剪形状绘制函数
const getClipFunction = (shape) => {
  switch (shape) {
    case 'heart':
      return drawHeart;
    case 'circle':
      return drawCircle;
    case 'star':
      return drawStar;
    case 'rectangle':
      return drawRectangle;
    default:
      return drawHeart;
  }
};

// 预渲染事件处理
const handlePrerender = (event) => {
  if (!enableClip.value) return;
  
  const ctx = event.context;
  const matrix = event.inversePixelTransform;
  const canvasPixelRatio = Math.sqrt(
    matrix[0] * matrix[0] + matrix[1] * matrix[1]
  );
  const canvasRotation = -Math.atan2(matrix[1], matrix[0]);
  
  ctx.save();
  
  // 居中画布并移除旋转以定位裁剪
  ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
  ctx.rotate(-canvasRotation);
  
  // 绘制裁剪路径
  const clipFunction = getClipFunction(clipShape.value);
  clipFunction(ctx, canvasPixelRatio * clipSize.value);
  
  ctx.clip();
  
  // 恢复变换
  if (clipShape.value === 'heart') {
    ctx.translate(75, 80);
    ctx.scale(1 / 3 / canvasPixelRatio / clipSize.value, 1 / 3 / canvasPixelRatio / clipSize.value);
  } else {
    ctx.scale(1 / canvasPixelRatio / clipSize.value, 1 / canvasPixelRatio / clipSize.value);
  }
  
  // 重新应用画布旋转和位置
  ctx.rotate(canvasRotation);
  ctx.translate(-ctx.canvas.width / 2, -ctx.canvas.height / 2);
};

// 后渲染事件处理
const handlePostrender = (event) => {
  if (!enableClip.value) return;
  
  const ctx = event.context;
  ctx.restore();
};

// 设置图层事件监听器
const setupLayerEvents = () => {
  if (osmLayer) {
    osmLayer.un('prerender', handlePrerender);
    osmLayer.un('postrender', handlePostrender);
    
    if (enableClip.value) {
      osmLayer.on('prerender', handlePrerender);
      osmLayer.on('postrender', handlePostrender);
    }
  }
};

// 更新裁剪形状
const updateClipShape = () => {
  setupLayerEvents();
  if (map) {
    map.render();
  }
};

// 更新裁剪大小
const updateClipSize = () => {
  if (map && enableClip.value) {
    map.render();
  }
};

// 切换裁剪
const toggleClip = () => {
  setupLayerEvents();
  if (map) {
    map.render();
  }
};

// 重置视图
const resetView = () => {
  if (map) {
    map.getView().animate({
      center: [116.4074, 39.9042],
      zoom: 10,
      duration: 1000
    });
  }
};

onMounted(() => {
  // 创建瓦片图层
  osmLayer = new TileLayer({
    source: new XYZ({
      url: "https://webrd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}",
    }),
  });

  // 创建视图
  const view = new View({
    center: [116.4074, 39.9042], // 北京市中心经纬度
    zoom: 10,
    projection: "EPSG:4326",
  });

  // 初始化地图
  map = new Map({
    target: mapContainer.value,
    layers: [osmLayer],
    view,
  });

  // 设置初始事件监听器
  setupLayerEvents();
});

onUnmounted(() => {
  // 清理事件监听器
  if (osmLayer) {
    osmLayer.un('prerender', handlePrerender);
    osmLayer.un('postrender', handlePostrender);
  }
  
  if (map) {
    map.setTarget(undefined);
    map = null;
  }
});
</script>

<style scoped>
.map-container {
  width: 100vw;
  height: 100vh;
  position: relative;
  font-family: sans-serif;
}

#map {
  width: 100%;
  height: 100%;
  background: #000; /* 黑色背景突出裁剪效果 */
}

.controls {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1000;
  background-color: rgba(255, 255, 255, 0.9);
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 220px;
}

.control-group {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
}

.control-group label {
  min-width: 80px;
  font-weight: 500;
  white-space: nowrap;
}

.control-group select {
  flex: 1;
  padding: 4px 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}

.control-group input[type="range"] {
  flex: 1;
  min-width: 100px;
}

.control-group input[type="checkbox"] {
  transform: scale(1.2);
}

.control-group span {
  min-width: 40px;
  text-align: right;
  font-weight: bold;
  color: #007bff;
}

.controls button {
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.3s ease;
}

.controls button:hover {
  background-color: #0056b3;
}
</style>