第13章 - 地图事件与动画

第13章 - 地图事件与动画

13.1 事件系统

13.1.1 地图事件

// 点击事件
map.on('click', (event) => {
  console.log('坐标:', event.coordinate);
  console.log('像素:', event.pixel);
});

// 单击(等待确认不是双击)
map.on('singleclick', (event) => {
  console.log('单击');
});

// 双击
map.on('dblclick', (event) => {
  event.preventDefault(); // 阻止默认缩放
});

// 指针移动
map.on('pointermove', (event) => {
  if (!event.dragging) {
    // 悬停检测
  }
});

// 地图移动
map.on('movestart', () => console.log('移动开始'));
map.on('moveend', () => console.log('移动结束'));

// 渲染事件
map.on('postrender', () => { /* 每帧渲染后 */ });
map.on('rendercomplete', () => { /* 渲染完成 */ });

13.1.2 视图事件

const view = map.getView();

// 属性变化
view.on('change:center', () => {
  console.log('中心点:', view.getCenter());
});

view.on('change:resolution', () => {
  console.log('缩放级别:', view.getZoom());
});

view.on('change:rotation', () => {
  console.log('旋转角度:', view.getRotation());
});

13.1.3 图层事件

// 图层可见性变化
layer.on('change:visible', () => {
  console.log('可见性:', layer.getVisible());
});

// 透明度变化
layer.on('change:opacity', () => {
  console.log('透明度:', layer.getOpacity());
});

// 矢量源事件
vectorSource.on('addfeature', (event) => {
  console.log('添加要素:', event.feature);
});

vectorSource.on('removefeature', (event) => {
  console.log('移除要素:', event.feature);
});

13.2 视图动画

13.2.1 基本动画

const view = map.getView();

// 平移动画
view.animate({
  center: fromLonLat([121.4737, 31.2304]),
  duration: 1000
});

// 缩放动画
view.animate({
  zoom: 15,
  duration: 500
});

// 旋转动画
view.animate({
  rotation: Math.PI / 2,
  duration: 500
});

// 组合动画
view.animate({
  center: fromLonLat([121.4737, 31.2304]),
  zoom: 15,
  rotation: 0,
  duration: 1500
});

13.2.2 缓动函数

import { easeIn, easeOut, inAndOut, linear } from 'ol/easing';

// 使用缓动函数
view.animate({
  center: destination,
  duration: 1000,
  easing: easeOut // 先快后慢
});

// 自定义缓动
function bounce(t) {
  const s = 7.5625, p = 2.75;
  if (t < 1/p) return s*t*t;
  if (t < 2/p) { t -= 1.5/p; return s*t*t + 0.75; }
  if (t < 2.5/p) { t -= 2.25/p; return s*t*t + 0.9375; }
  t -= 2.625/p;
  return s*t*t + 0.984375;
}

view.animate({
  center: destination,
  duration: 1500,
  easing: bounce
});

13.2.3 连续动画

// 按顺序执行
view.animate(
  { center: point1, duration: 500 },
  { center: point2, duration: 500 },
  { center: point3, duration: 500 }
);

// 飞行效果
function flyTo(destination, zoom) {
  const startZoom = view.getZoom();
  const minZoom = Math.min(startZoom, zoom, 5);
  
  view.animate(
    { center: destination, duration: 1000 },
    callback
  );
  
  view.animate(
    { zoom: minZoom, duration: 500 },
    { zoom: zoom, duration: 500 },
    callback
  );
  
  let parts = 2;
  function callback(complete) {
    if (--parts === 0) {
      console.log('飞行完成');
    }
  }
}

13.3 要素动画

13.3.1 位置动画

// 要素移动动画
function animateFeature(feature, destination, duration = 1000) {
  const geometry = feature.getGeometry();
  const start = geometry.getCoordinates();
  const startTime = Date.now();
  
  function animate() {
    const elapsed = Date.now() - startTime;
    const progress = Math.min(elapsed / duration, 1);
    
    // 使用缓动
    const eased = easeOut(progress);
    
    const x = start[0] + (destination[0] - start[0]) * eased;
    const y = start[1] + (destination[1] - start[1]) * eased;
    
    geometry.setCoordinates([x, y]);
    
    if (progress < 1) {
      requestAnimationFrame(animate);
    }
  }
  
  animate();
}

// 沿路径移动
function animateAlongPath(feature, path, duration = 5000) {
  const startTime = Date.now();
  
  function animate() {
    const elapsed = Date.now() - startTime;
    const progress = Math.min(elapsed / duration, 1);
    
    // 计算当前位置
    const point = path.getCoordinateAt(progress);
    feature.getGeometry().setCoordinates(point);
    
    if (progress < 1) {
      requestAnimationFrame(animate);
      map.render();
    }
  }
  
  animate();
}

13.3.2 样式动画

// 脉冲效果
function createPulseStyle() {
  let radius = 5;
  let increasing = true;
  
  return function(feature) {
    if (increasing) {
      radius += 0.5;
      if (radius >= 15) increasing = false;
    } else {
      radius -= 0.5;
      if (radius <= 5) increasing = true;
    }
    
    return new Style({
      image: new Circle({
        radius: radius,
        fill: new Fill({ color: 'rgba(255, 0, 0, 0.5)' }),
        stroke: new Stroke({ color: 'red', width: 2 })
      })
    });
  };
}

// 闪烁效果
function createBlinkStyle() {
  return function(feature) {
    const visible = Math.floor(Date.now() / 500) % 2 === 0;
    
    return visible ? new Style({
      image: new Circle({
        radius: 8,
        fill: new Fill({ color: 'red' })
      })
    }) : null;
  };
}

// 需要持续渲染
function startAnimation() {
  map.on('postrender', () => map.render());
}

13.4 轨迹回放

class TrackPlayer {
  constructor(map, layer, coordinates) {
    this.map = map;
    this.layer = layer;
    this.coordinates = coordinates;
    this.currentIndex = 0;
    this.playing = false;
    this.speed = 1;
    
    // 创建移动的点要素
    this.marker = new Feature({
      geometry: new Point(coordinates[0])
    });
    
    this.marker.setStyle(new Style({
      image: new Icon({
        src: '/images/car.png',
        rotation: 0
      })
    }));
    
    layer.getSource().addFeature(this.marker);
  }
  
  play() {
    this.playing = true;
    this.animate();
  }
  
  pause() {
    this.playing = false;
  }
  
  stop() {
    this.playing = false;
    this.currentIndex = 0;
    this.updatePosition();
  }
  
  setSpeed(speed) {
    this.speed = speed;
  }
  
  animate() {
    if (!this.playing) return;
    if (this.currentIndex >= this.coordinates.length - 1) {
      this.playing = false;
      return;
    }
    
    const start = this.coordinates[this.currentIndex];
    const end = this.coordinates[this.currentIndex + 1];
    
    // 计算方向
    const dx = end[0] - start[0];
    const dy = end[1] - start[1];
    const rotation = -Math.atan2(dy, dx);
    
    // 更新图标旋转
    this.marker.getStyle().getImage().setRotation(rotation);
    
    // 移动到下一点
    this.currentIndex++;
    this.updatePosition();
    
    // 延迟后继续
    setTimeout(() => this.animate(), 100 / this.speed);
  }
  
  updatePosition() {
    const coord = this.coordinates[this.currentIndex];
    this.marker.getGeometry().setCoordinates(coord);
    
    // 地图跟随
    this.map.getView().setCenter(coord);
  }
}

// 使用示例
const coordinates = [/* 轨迹坐标数组 */];
const player = new TrackPlayer(map, vectorLayer, coordinates);

document.getElementById('play-btn').onclick = () => player.play();
document.getElementById('pause-btn').onclick = () => player.pause();
document.getElementById('stop-btn').onclick = () => player.stop();

13.5 地图截图与导出

// 导出为图片
function exportMapAsImage(map, filename = 'map.png') {
  map.once('rendercomplete', () => {
    const canvas = document.createElement('canvas');
    const size = map.getSize();
    canvas.width = size[0];
    canvas.height = size[1];
    const context = canvas.getContext('2d');
    
    document.querySelectorAll('.ol-layer canvas').forEach(layerCanvas => {
      if (layerCanvas.width > 0) {
        const opacity = layerCanvas.parentNode.style.opacity || 1;
        context.globalAlpha = opacity;
        context.drawImage(layerCanvas, 0, 0);
      }
    });
    
    // 下载
    const link = document.createElement('a');
    link.download = filename;
    link.href = canvas.toDataURL();
    link.click();
  });
  
  map.renderSync();
}

// 复制到剪贴板
async function copyMapToClipboard(map) {
  map.once('rendercomplete', async () => {
    const canvas = document.createElement('canvas');
    const size = map.getSize();
    canvas.width = size[0];
    canvas.height = size[1];
    const context = canvas.getContext('2d');
    
    document.querySelectorAll('.ol-layer canvas').forEach(layerCanvas => {
      if (layerCanvas.width > 0) {
        context.drawImage(layerCanvas, 0, 0);
      }
    });
    
    canvas.toBlob(async (blob) => {
      await navigator.clipboard.write([
        new ClipboardItem({ 'image/png': blob })
      ]);
      console.log('已复制到剪贴板');
    });
  });
  
  map.renderSync();
}

13.6 本章小结

本章介绍了地图事件与动画:

  1. 事件系统:地图、视图、图层事件
  2. 视图动画:平移、缩放、旋转、缓动函数
  3. 要素动画:位置动画、样式动画
  4. 轨迹回放:沿路径移动、播放控制
  5. 地图导出:截图、剪贴板

关键要点

  • 使用 view.animate() 实现平滑过渡
  • 缓动函数控制动画节奏
  • requestAnimationFrame 实现自定义动画

← 上一章:Overlay覆盖物 | 返回目录 | 下一章:投影与坐标转换 →

posted @ 2026-01-08 11:37  我才是银古  阅读(13)  评论(0)    收藏  举报