diff --git a/README.md b/README.md
index a336d81..f511eb3 100644
--- a/README.md
+++ b/README.md
@@ -238,3 +238,54 @@
类参考
+
+
+轨迹动画(TrackAnimation)
+---------
+
+
+时间轴(Timeline)
+---------
+
+
+
+
+ |
+
+ Timeline 提供时间轴控件能力,可播放/暂停,方便结合地图做时序数据展示。
+时间轴示例
+源码
+压缩源码
+样式
+类参考
+ |
+
+
+卷帘对比(Swipe)
+---------
+
\ No newline at end of file
diff --git a/demo/Swipe/Swipe.html b/demo/Swipe/Swipe.html
new file mode 100644
index 0000000..48deac4
--- /dev/null
+++ b/demo/Swipe/Swipe.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ Swipe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/Timeline/Timeline.html b/demo/Timeline/Timeline.html
new file mode 100644
index 0000000..9986e1f
--- /dev/null
+++ b/demo/Timeline/Timeline.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ Timeline
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/TrackAnimation/TrackAnimation.html b/demo/TrackAnimation/TrackAnimation.html
new file mode 100644
index 0000000..3a7bf36
--- /dev/null
+++ b/demo/TrackAnimation/TrackAnimation.html
@@ -0,0 +1,65 @@
+
+
+
+
+ 轨迹播放
+
+
+
+
+
+
+
+
+
+
+
+ 速度:
+
+
+
+
+
+
+
+
diff --git a/images/Swipe.png b/images/Swipe.png
new file mode 100644
index 0000000..06b5eee
Binary files /dev/null and b/images/Swipe.png differ
diff --git a/images/Timeline.png b/images/Timeline.png
new file mode 100644
index 0000000..7e4497a
Binary files /dev/null and b/images/Timeline.png differ
diff --git a/images/TrackAnimation.png b/images/TrackAnimation.png
new file mode 100644
index 0000000..76a7041
Binary files /dev/null and b/images/TrackAnimation.png differ
diff --git a/src/Swipe/Swipe.css b/src/Swipe/Swipe.css
new file mode 100644
index 0000000..6e2f7f0
--- /dev/null
+++ b/src/Swipe/Swipe.css
@@ -0,0 +1,22 @@
+.BMapLib-swipe {
+ position: absolute;
+ height: 100%;
+ z-index: 1;
+ top: 0;
+ left: 50%;
+ will-change: transform;
+}
+
+.BMapLib-swipe-btn {
+ position: absolute;
+ top: 50%;
+ left: -25px;
+ height: 50px;
+ width: 50px;
+ background-color: aliceblue;
+ border-radius: 25px;
+ cursor: ew-resize;
+ background-image: url(data:image/png;base64,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);
+ background-size: 50px;
+}
+
diff --git a/src/Swipe/Swipe.js b/src/Swipe/Swipe.js
new file mode 100644
index 0000000..b8b4c81
--- /dev/null
+++ b/src/Swipe/Swipe.js
@@ -0,0 +1,274 @@
+/**
+ * @fileoverview Swipe卷帘对比
+ * 支持两张 BMap.Map 叠加显示,通过拖拽中线裁剪左右显示区域,并同步两张地图的中心与缩放。
+ */
+let BMapLib = window.BMapLib || {};
+
+const prefix = 'BMapLib';
+
+class Swipe {
+ /**
+ * @param {BMap.Map} mapA - 左侧地图
+ * @param {BMap.Map} mapB - 右侧地图
+ * @param {HTMLElement} wrapper - 外层容器(需包含两张地图的容器节点)
+ * @param {Object} [options]
+ * @param {number} [options.initialX] - 初始分割线位置(px,相对于 wrapper 左侧);默认居中
+ */
+ constructor(mapA, mapB, wrapper, options = {}) {
+ if (!mapA || !mapB || !wrapper) {
+ throw new Error('BMapLib.Swipe: mapA, mapB, wrapper are required');
+ }
+ this._mapA = mapA;
+ this._mapB = mapB;
+ this._wrapper = wrapper;
+ this._options = options || {};
+
+ this._wrapperRect = null;
+ this._centerX = 0;
+ this._wrapperWidth = 0;
+
+ this._dragging = false;
+ this._lastX = 0;
+
+ // 同步保护与节流
+ this._isSyncingFrom = null;
+ this._rafSyncId = 0;
+ this._pendingSync = null;
+
+ this._initDom();
+
+ this._syncMapA = this._syncMapA.bind(this);
+ this._syncMapB = this._syncMapB.bind(this);
+ this._onSync();
+ }
+
+ _initDom() {
+ this._swipeControlDiv = document.createElement('div');
+ this._swipeControlDiv.className = `${prefix}-swipe`;
+
+ this._swipeBtn = document.createElement('div');
+ this._swipeBtn.className = `${prefix}-swipe-btn`;
+ this._swipeControlDiv.appendChild(this._swipeBtn);
+
+ this._onSwipeStart = this._onSwipeStart.bind(this);
+ this._onSwipeMove = this._onSwipeMove.bind(this);
+ this._onSwipeEnd = this._onSwipeEnd.bind(this);
+
+ this._swipeBtn.addEventListener('mousedown', this._onSwipeStart);
+
+ this._wrapper.appendChild(this._swipeControlDiv);
+
+ this._recalcWrapper();
+ const initX =
+ typeof this._options.initialX === 'number'
+ ? this._options.initialX
+ : Math.floor(this._wrapperWidth / 2);
+ this._setX(initX);
+ }
+
+ _recalcWrapper() {
+ this._wrapperRect = this._wrapper.getBoundingClientRect();
+ this._centerX = this._wrapperRect.left;
+ this._wrapperWidth = this._wrapperRect.width;
+ }
+
+ _onSwipeStart(e) {
+ this._dragging = true;
+ this._recalcWrapper();
+ document.addEventListener('mousemove', this._onSwipeMove);
+ document.addEventListener('mouseup', this._onSwipeEnd);
+ if (e && e.preventDefault) e.preventDefault();
+ }
+
+ _onSwipeMove(e) {
+ if (!this._dragging) return;
+ const x = e.clientX - this._centerX;
+ this._setX(x);
+ }
+
+ _onSwipeEnd() {
+ this._dragging = false;
+ document.removeEventListener('mousemove', this._onSwipeMove);
+ document.removeEventListener('mouseup', this._onSwipeEnd);
+ }
+
+ _setX(x) {
+ this._recalcWrapper();
+ const clamped = Math.max(0, Math.min(this._wrapperWidth, x));
+ this._lastX = clamped;
+
+ // 控制线位置:left:50% + translateX(...)
+ const offset = clamped - this._wrapperWidth / 2;
+ this._swipeControlDiv.style.transform = `translateX(${offset}px)`;
+
+ this._updateClip(clamped);
+ }
+
+ _supportsClipPath() {
+ const style = document.createElement('div').style;
+ return 'clipPath' in style || 'webkitClipPath' in style;
+ }
+
+ _updateClip(x) {
+ const containerA = this._mapA.getContainer();
+ const containerB = this._mapB.getContainer();
+
+ if (!containerA || !containerB) return;
+
+ // 优先clip-path(现代浏览器),不支持则回退到clip:rect()
+ if (this._supportsClipPath()) {
+ const clipA = `polygon(0 0, ${x}px 0, ${x}px 100%, 0 100%)`;
+ const clipB = `polygon(${x}px 0, 100% 0, 100% 100%, ${x}px 100%)`;
+ containerA.style.clipPath = clipA;
+ containerB.style.clipPath = clipB;
+ containerA.style.webkitClipPath = clipA;
+ containerB.style.webkitClipPath = clipB;
+ } else {
+ // clip只对position:absolute生效
+ const w = this._wrapperWidth;
+ containerA.style.clip = `rect(0px, ${x}px, auto, 0px)`;
+ containerB.style.clip = `rect(0px, ${w}px, auto, ${x}px)`;
+ }
+ }
+
+ _onSync() {
+ // moving/zooming用于实时同步;zoomend/moveend作兜底
+ this._mapA.addEventListener('moving', this._syncMapA);
+ this._mapA.addEventListener('zooming', this._syncMapA);
+ this._mapA.addEventListener('moveend', this._syncMapA);
+ this._mapA.addEventListener('zoomend', this._syncMapA);
+
+ this._mapB.addEventListener('moving', this._syncMapB);
+ this._mapB.addEventListener('zooming', this._syncMapB);
+ this._mapB.addEventListener('moveend', this._syncMapB);
+ this._mapB.addEventListener('zoomend', this._syncMapB);
+ }
+
+ _offSync() {
+ this._mapA.removeEventListener('moving', this._syncMapA);
+ this._mapA.removeEventListener('zooming', this._syncMapA);
+ this._mapA.removeEventListener('moveend', this._syncMapA);
+ this._mapA.removeEventListener('zoomend', this._syncMapA);
+
+ this._mapB.removeEventListener('moving', this._syncMapB);
+ this._mapB.removeEventListener('zooming', this._syncMapB);
+ this._mapB.removeEventListener('moveend', this._syncMapB);
+ this._mapB.removeEventListener('zoomend', this._syncMapB);
+ }
+
+ _getEventMode() {
+ const ev = window.event;
+ const type = ev && ev.type;
+ if (type === 'moving') return 'move';
+ if (type === 'zooming') return 'zoom';
+ if (type === 'moveend' || type === 'zoomend') return 'end';
+ return 'end';
+ }
+
+ _scheduleSync(from, center, zoom, mode) {
+ this._pendingSync = { from, center, zoom, mode };
+ if (this._rafSyncId) return;
+ const that = this;
+ this._rafSyncId = requestAnimationFrame(function () {
+ that._rafSyncId = 0;
+ const p = that._pendingSync;
+ that._pendingSync = null;
+ if (!p) return;
+ that._applySync(p.from, p.center, p.zoom, p.mode);
+ });
+ }
+
+ _applySync(from, center, zoom, mode) {
+ if (this._isSyncingFrom && this._isSyncingFrom !== from) {
+ // 另一侧正在同步,丢弃本次(避免抖动/互相抢控)
+ return;
+ }
+
+ this._isSyncingFrom = from;
+ const target = from === 'A' ? this._mapB : this._mapA;
+
+ try {
+ // 关键:移动时只同步中心,避免centerAndZoom带来的动画/重算误差
+ if (mode === 'move') {
+ target.setCenter(center);
+ } else if (mode === 'zoom') {
+ // zooming阶段只同步zoom,减少“拖影”
+ target.setZoom(zoom);
+ target.setCenter(center);
+ } else {
+ // end:确保center/zoom全对齐
+ target.setZoom(zoom);
+ target.setCenter(center);
+ }
+ } finally {
+ // 释放同步锁:下一帧再释放,避免同一事件循环内回调互相触发
+ const that = this;
+ setTimeout(function () {
+ that._isSyncingFrom = null;
+ }, 0);
+ }
+ }
+
+ _syncMapA() {
+ if (this._isSyncingFrom && this._isSyncingFrom !== 'A') return;
+ const zoom = this._mapA.getZoom();
+ const center = this._mapA.getCenter();
+ const mode = this._getEventMode();
+ this._scheduleSync('A', center, zoom, mode);
+ }
+
+ _syncMapB() {
+ if (this._isSyncingFrom && this._isSyncingFrom !== 'B') return;
+ const zoom = this._mapB.getZoom();
+ const center = this._mapB.getCenter();
+ const mode = this._getEventMode();
+ this._scheduleSync('B', center, zoom, mode);
+ }
+
+ /**
+ * 主动设置分割线位置(0~wrapper宽度 px)
+ * @param {number} x
+ */
+ setPosition(x) {
+ this._setX(x);
+ }
+
+ /**
+ * 获取当前分割线位置
+ * @returns {number}
+ */
+ getPosition() {
+ return this._lastX;
+ }
+
+ destroy() {
+ this._offSync();
+ if (this._rafSyncId) {
+ cancelAnimationFrame(this._rafSyncId);
+ this._rafSyncId = 0;
+ }
+ if (this._swipeBtn) {
+ this._swipeBtn.removeEventListener('mousedown', this._onSwipeStart);
+ }
+ this._onSwipeEnd();
+ if (this._swipeControlDiv && this._swipeControlDiv.parentNode) {
+ this._swipeControlDiv.parentNode.removeChild(this._swipeControlDiv);
+ }
+ // 清理裁剪
+ const containerA = this._mapA && this._mapA.getContainer && this._mapA.getContainer();
+ const containerB = this._mapB && this._mapB.getContainer && this._mapB.getContainer();
+ if (containerA) {
+ containerA.style.clipPath = '';
+ containerA.style.webkitClipPath = '';
+ containerA.style.clip = '';
+ }
+ if (containerB) {
+ containerB.style.clipPath = '';
+ containerB.style.webkitClipPath = '';
+ containerB.style.clip = '';
+ }
+ }
+}
+
+BMapLib.Swipe = Swipe;
+
diff --git a/src/Swipe/Swipe.min.js b/src/Swipe/Swipe.min.js
new file mode 100644
index 0000000..c4c8cf8
--- /dev/null
+++ b/src/Swipe/Swipe.min.js
@@ -0,0 +1 @@
+let BMapLib=window.BMapLib||{};const prefix="BMapLib";class Swipe{constructor(t,i,e,n={}){if(!t||!i||!e)throw new Error("BMapLib.Swipe: mapA, mapB, wrapper are required");this._mapA=t,this._mapB=i,this._wrapper=e,this._options=n||{},this._wrapperRect=null,this._centerX=0,this._wrapperWidth=0,this._dragging=!1,this._lastX=0,this._isSyncingFrom=null,this._rafSyncId=0,this._pendingSync=null,this._initDom(),this._syncMapA=this._syncMapA.bind(this),this._syncMapB=this._syncMapB.bind(this),this._onSync()}_initDom(){this._swipeControlDiv=document.createElement("div"),this._swipeControlDiv.className=`${prefix}-swipe`,this._swipeBtn=document.createElement("div"),this._swipeBtn.className=`${prefix}-swipe-btn`,this._swipeControlDiv.appendChild(this._swipeBtn),this._onSwipeStart=this._onSwipeStart.bind(this),this._onSwipeMove=this._onSwipeMove.bind(this),this._onSwipeEnd=this._onSwipeEnd.bind(this),this._swipeBtn.addEventListener("mousedown",this._onSwipeStart),this._wrapper.appendChild(this._swipeControlDiv),this._recalcWrapper();const t="number"==typeof this._options.initialX?this._options.initialX:Math.floor(this._wrapperWidth/2);this._setX(t)}_recalcWrapper(){this._wrapperRect=this._wrapper.getBoundingClientRect(),this._centerX=this._wrapperRect.left,this._wrapperWidth=this._wrapperRect.width}_onSwipeStart(t){this._dragging=!0,this._recalcWrapper(),document.addEventListener("mousemove",this._onSwipeMove),document.addEventListener("mouseup",this._onSwipeEnd),t&&t.preventDefault&&t.preventDefault()}_onSwipeMove(t){if(!this._dragging)return;const i=t.clientX-this._centerX;this._setX(i)}_onSwipeEnd(){this._dragging=!1,document.removeEventListener("mousemove",this._onSwipeMove),document.removeEventListener("mouseup",this._onSwipeEnd)}_setX(t){this._recalcWrapper();const i=Math.max(0,Math.min(this._wrapperWidth,t));this._lastX=i;const e=i-this._wrapperWidth/2;this._swipeControlDiv.style.transform=`translateX(${e}px)`,this._updateClip(i)}_supportsClipPath(){const t=document.createElement("div").style;return"clipPath"in t||"webkitClipPath"in t}_updateClip(t){const i=this._mapA.getContainer(),e=this._mapB.getContainer();if(i&&e)if(this._supportsClipPath()){const n=`polygon(0 0, ${t}px 0, ${t}px 100%, 0 100%)`,s=`polygon(${t}px 0, 100% 0, 100% 100%, ${t}px 100%)`;i.style.clipPath=n,e.style.clipPath=s,i.style.webkitClipPath=n,e.style.webkitClipPath=s}else{const n=this._wrapperWidth;i.style.clip=`rect(0px, ${t}px, auto, 0px)`,e.style.clip=`rect(0px, ${n}px, auto, ${t}px)`}}_onSync(){this._mapA.addEventListener("moving",this._syncMapA),this._mapA.addEventListener("zooming",this._syncMapA),this._mapA.addEventListener("moveend",this._syncMapA),this._mapA.addEventListener("zoomend",this._syncMapA),this._mapB.addEventListener("moving",this._syncMapB),this._mapB.addEventListener("zooming",this._syncMapB),this._mapB.addEventListener("moveend",this._syncMapB),this._mapB.addEventListener("zoomend",this._syncMapB)}_offSync(){this._mapA.removeEventListener("moving",this._syncMapA),this._mapA.removeEventListener("zooming",this._syncMapA),this._mapA.removeEventListener("moveend",this._syncMapA),this._mapA.removeEventListener("zoomend",this._syncMapA),this._mapB.removeEventListener("moving",this._syncMapB),this._mapB.removeEventListener("zooming",this._syncMapB),this._mapB.removeEventListener("moveend",this._syncMapB),this._mapB.removeEventListener("zoomend",this._syncMapB)}_getEventMode(){const t=window.event,i=t&&t.type;return"moving"===i?"move":"zooming"===i?"zoom":"end"}_scheduleSync(t,i,e,n){if(this._pendingSync={from:t,center:i,zoom:e,mode:n},this._rafSyncId)return;const s=this;this._rafSyncId=requestAnimationFrame(function(){s._rafSyncId=0;const t=s._pendingSync;s._pendingSync=null,t&&s._applySync(t.from,t.center,t.zoom,t.mode)})}_applySync(t,i,e,n){if(this._isSyncingFrom&&this._isSyncingFrom!==t)return;this._isSyncingFrom=t;const s="A"===t?this._mapB:this._mapA;try{"move"===n||s.setZoom(e),s.setCenter(i)}finally{const t=this;setTimeout(function(){t._isSyncingFrom=null},0)}}_syncMapA(){if(this._isSyncingFrom&&"A"!==this._isSyncingFrom)return;const t=this._mapA.getZoom(),i=this._mapA.getCenter(),e=this._getEventMode();this._scheduleSync("A",i,t,e)}_syncMapB(){if(this._isSyncingFrom&&"B"!==this._isSyncingFrom)return;const t=this._mapB.getZoom(),i=this._mapB.getCenter(),e=this._getEventMode();this._scheduleSync("B",i,t,e)}setPosition(t){this._setX(t)}getPosition(){return this._lastX}destroy(){this._offSync(),this._rafSyncId&&(cancelAnimationFrame(this._rafSyncId),this._rafSyncId=0),this._swipeBtn&&this._swipeBtn.removeEventListener("mousedown",this._onSwipeStart),this._onSwipeEnd(),this._swipeControlDiv&&this._swipeControlDiv.parentNode&&this._swipeControlDiv.parentNode.removeChild(this._swipeControlDiv);const t=this._mapA&&this._mapA.getContainer&&this._mapA.getContainer(),i=this._mapB&&this._mapB.getContainer&&this._mapB.getContainer();t&&(t.style.clipPath="",t.style.webkitClipPath="",t.style.clip=""),i&&(i.style.clipPath="",i.style.webkitClipPath="",i.style.clip="")}}BMapLib.Swipe=Swipe;
\ No newline at end of file
diff --git a/src/Timeline/Timeline.css b/src/Timeline/Timeline.css
new file mode 100644
index 0000000..d4dceaf
--- /dev/null
+++ b/src/Timeline/Timeline.css
@@ -0,0 +1,80 @@
+.BMapLib-timeline {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ z-index: 66;
+ display: flex;
+ align-items: center;
+ height: 70px;
+ border-radius: 5px;
+ background-color: rgba(0, 0, 0, .5);
+ padding: 0 12px;
+ box-shadow: 1px 1px 3px 1px #cbcbcb;
+}
+
+.BMapLib-timeline-play {
+ color: #fff;
+ font-size: 16px;
+ margin-right: 8px;
+ cursor: pointer;
+}
+
+.BMapLib-timeline-main {
+ /* width: 500px; */
+ position: relative;
+ overflow: hidden;
+}
+
+.BMapLib-timeline-main ul {
+ display: flex;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ /* height: 40px; */
+}
+
+.BMapLib-timeline-main ul li {
+ position: relative;
+ font-size: 14px;
+ color: #fff;
+ text-align: center;
+}
+
+.BMapLib-timeline-main ul li span {
+ user-select: none;
+}
+
+.BMapLib-timeline-progress {
+ height: 10px;
+ width: 10px;
+ background-color: #fff;
+ border-radius: 5px;
+ cursor: pointer;
+ position: absolute;
+ left: 0;
+ top: 10px;
+}
+
+.BMapLib-time-item {
+ height: 10px;
+ margin-top: 10px;
+ background-color: #C1C3C6;
+ box-sizing: content-box;
+}
+
+.BMapLib-time-start {
+ border-radius: 5px 0 0 5px;
+}
+
+.BMapLib-time-end {
+ border-radius: 0 5px 5px 0;
+}
+
+.BMapLib-time-divider {
+ position: absolute;
+ top: 1px;
+ left: 50%;
+ width: 1px;
+ height: 19px;
+ background-color: #fff;
+}
\ No newline at end of file
diff --git a/src/Timeline/Timeline.js b/src/Timeline/Timeline.js
new file mode 100644
index 0000000..b1f43b9
--- /dev/null
+++ b/src/Timeline/Timeline.js
@@ -0,0 +1,370 @@
+let BMapLib = window.BMapLib || {};
+const prefix = 'BMapLib';
+class Timeline {
+ /**
+ * @param {TimelineOptions} options
+ */
+ constructor(options) {
+ /**
+ * @type {TimelineOptions}
+ */
+ this.options = options;
+ this.listeners = {};
+ this._interval = options.interval || 1000;
+ this._playTimeId = null;
+ this._playStatus = 'pause';
+ this._startX; // 初始进度按钮距离视口左边距离
+ this._progressMax = 0; // 进度条总长度
+ this._playIndex = 0;
+ this._progress = 0;
+ /**
+ * @type {Step[]}
+ */
+ this._steps = [];
+ this._times = options.times || [];
+ this._ctx = document.createElement('canvas').getContext('2d');
+ this._startScrollIndex = -1;
+ this._scrollIndex = 1;
+ this._container = null;
+
+ this._playButton = options.playButton ? options.playButton : document.createTextNode('播放');
+ this._pauseButton = options.pauseButton ? options.pauseButton : document.createTextNode('暂停');
+
+ this._initDom(options);
+
+ this._drawTime();
+ }
+
+ /**
+ * @param {TimelineOptions} options
+ */
+ _initDom() {
+ this.element = document.createElement('div');
+ this.element.className = `${prefix}-timeline`;
+ if (this.options.className) {
+ this.element.className += ` ${this.options.className}`;
+ }
+
+ this._startButton = document.createElement('div');
+ this._startButton.className = `${prefix}-timeline-play`;
+ this._startButton.innerHTML = '';
+ this._startButton.appendChild(this._playButton);
+ this.element.appendChild(this._startButton);
+
+ this._scrollDiv = document.createElement('div');
+ this._scrollDiv.className = `${prefix}-timeline-main`;
+ this.element.appendChild(this._scrollDiv);
+
+ this._ul = document.createElement('ul');
+ this._scrollDiv.appendChild(this._ul);
+
+ this._progressDiv = document.createElement('div');
+ this._progressDiv.className = `${prefix}-timeline-progress`;
+ if (this.options.progressButtonStyle) {
+ this._applyStyle(this._progressDiv, this.options.progressButtonStyle);
+ }
+ this._scrollDiv.appendChild(this._progressDiv);
+
+ this._onPlayChange = this._onPlayChange.bind(this);
+ this._onProgressDragStart = this._onProgressDragStart.bind(this);
+ this._onProgressDrag = this._onProgressDrag.bind(this);
+ this._onProgressDragEnd = this._onProgressDragEnd.bind(this);
+ this._startButton.addEventListener('click', this._onPlayChange);
+ this._progressDiv.addEventListener('mousedown', this._onProgressDragStart);
+
+ if (this.options.customContainer) {
+ this._container = this.options.customContainer;
+ this.options.customContainer.appendChild(this.element);
+ }
+ else if (this.options.map) {
+ this._container = this.options.map.getContainer();
+ this.options.map.getContainer().appendChild(this.element);
+ }
+ else {
+ throw new Error('options.map or options.customContainer is required');
+ }
+ this._startX = this._scrollDiv.getBoundingClientRect().left;
+ }
+
+ on(eventName, callback) {
+ if (!this.listeners[eventName]) {
+ this.listeners[eventName] = [];
+ }
+ this.listeners[eventName].push(callback);
+ }
+
+ un(eventName, callback) {
+ if (this.listeners[eventName]) {
+ let index = this.listeners[eventName].indexOf(callback);
+ if (index > -1) {
+ this.listeners[eventName].splice(index, 1);
+ }
+ }
+ }
+
+ dispatchEvent(event) {
+ if (this.listeners[event.type]) {
+ for (let i = 0; i < this.listeners[event.type].length; i++) {
+ this.listeners[event.type][i](event);
+ }
+ }
+ }
+
+ pause() {
+ if (this._playStatus === 'pause') {
+ return;
+ }
+ this._onPlayChange();
+ }
+
+ play() {
+ if (this._playStatus === 'play') {
+ return;
+ }
+ this._onPlayChange();
+ }
+
+ /**
+ * @private
+ */
+ _drawTime() {
+ let start = 0;
+ let px = 0;
+ for (let i = 0; i < this._times.length; i++) {
+ const time = this._times[i];
+ const li = document.createElement('li');
+ const timeItem = document.createElement('div');
+ const span = document.createElement('span');
+ span.innerText = time;
+ if (this.options.timeStyle) {
+ this._applyStyle(span, this.options.timeStyle);
+ }
+ li.appendChild(timeItem);
+ li.appendChild(span);
+
+
+ timeItem.className = `${prefix}-time-item`;
+ if (i === 0) {
+ timeItem.className = `${prefix}-time-item ${prefix}-time-start`;
+ }
+ if (i === this._times.length - 1) {
+ timeItem.className = `${prefix}-time-item ${prefix}-time-end`;
+ }
+ if (this.options.scrollStyle) {
+ this._applyStyle(timeItem, this.options.scrollStyle);
+ }
+
+ const divider = document.createElement('div');
+ divider.className = `${prefix}-time-divider`;
+ if (this.options.dividerStyle) {
+ this._applyStyle(divider, this.options.dividerStyle);
+ }
+ li.appendChild(divider);
+
+ this._ul.appendChild(li);
+ this._ctx.font = getComputedStyle(span).font;
+ const textWidth = this._ctx.measureText(time).width;
+
+ const stepWidth = textWidth + 16;
+ timeItem.style.width = stepWidth + 'px';
+
+ let halfWidth = stepWidth / 2;
+ px += stepWidth;
+ const timeInfo = {
+ time,
+ start,
+ end: px - halfWidth,
+ index: i,
+ };
+ start = px - halfWidth + 1;
+ this._steps.push(timeInfo);
+
+ }
+ this._progressMax = px;
+ this._calcStartScrollIndex();
+ }
+
+ /**
+ * @private
+ */
+ _calcStartScrollIndex() {
+ const scrollWidth = this._scrollDiv.clientWidth;
+ let startScrollIndex = 0;
+ for (let i = 0; i < this._steps.length - 1; i++) {
+ if (this._steps[i].end > scrollWidth) {
+ startScrollIndex = i;
+ break;
+ }
+ }
+ this._startScrollIndex = startScrollIndex;
+ // console.log(startScrollIndex, this._steps);
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} e
+ * @returns
+ */
+ _onProgressDrag(e) {
+ const x = e.clientX - this._startX + this._scrollDiv.scrollLeft;
+
+ if (x >= 0 && x <= this._progressMax) {
+ this._updateProgress(x);
+ }
+ }
+
+ // 进度拖拽结束
+ _onProgressDragEnd(e) {
+ const x = e.clientX - this._startX;
+ const step = this._getStepByProgress(x);
+ if (step) {
+ this.dispatchEvent({
+ type: 'change',
+ time: step.time,
+ });
+ this._playIndex = step.index;
+ } else {
+ this._playIndex = this._steps.length;
+ }
+ document.removeEventListener('mousemove', this._onProgressDrag);
+ document.removeEventListener('mouseup', this._onProgressDragEnd);
+ }
+
+ // 点击进度按钮
+ _onProgressDragStart(e) {
+ clearTimeout(this._playTimeId);
+ this._playStatus = 'pause';
+ this._startButton.innerHTML = '';
+ this._startButton.appendChild(this._playButton);
+ document.addEventListener('mousemove', this._onProgressDrag);
+ document.addEventListener('mouseup', this._onProgressDragEnd);
+ }
+
+ // 点击播放按钮
+ _onPlayChange() {
+ if (this._playTimeId) {
+ clearTimeout(this._playTimeId);
+ }
+
+ // 播放到结尾了,重置
+ if (this._playIndex >= this._steps.length) {
+ this._playIndex = 0;
+ this._scrollIndex = 1;
+ this._scrollDiv.scrollLeft = 0;
+ this._progressDiv.style.left = 0;
+ } else {
+ let dragStep = this._getStepByProgress(this._progress);
+ if (dragStep) {
+ this._playIndex = dragStep.index;
+ }
+ else {
+ this._playIndex = this._steps.length;
+ }
+ }
+ if (this._playStatus === 'pause') {
+ this.dispatchEvent({
+ type: 'playstart'
+ });
+
+ const loop = () => {
+ this._playTimeId = setTimeout(loop, this._interval);
+ let step = this._steps[this._playIndex++];
+
+ step && this._updateProgressByStep(step);
+ if (this._playIndex >= this._steps.length) {
+ clearTimeout(this._playTimeId);
+ this._playStatus = 'pause';
+ this._startButton.innerHTML = '';
+ this._startButton.appendChild(this._playButton);
+ this.dispatchEvent({
+ type: 'playend'
+ });
+ }
+ }
+
+ this._playTimeId = setTimeout(loop, this._interval);
+ this._playStatus = 'play';
+ this._startButton.innerHTML = '';
+ this._startButton.appendChild(this._pauseButton);
+ } else {
+ this._playStatus = 'pause';
+ this._startButton.innerHTML = '';
+ this._startButton.appendChild(this._playButton);
+ this.dispatchEvent({
+ type: 'playend'
+ });
+ clearTimeout(this._playTimeId);
+ }
+ }
+
+ /**
+ * @param {Step} step
+ */
+ _updateProgressByStep(step) {
+ this.dispatchEvent({
+ type: 'change',
+ time: step.time,
+ });
+
+ this._progress = step.end;
+ this._progressDiv.style.left = this._progress - 5 + 'px';
+
+ // console.log(this._scrollIndex);
+ // 判断是否滚动
+ if (this._startScrollIndex > 0 && step.end >= this._steps[this._startScrollIndex].end) {
+ this._scrollDiv.scrollLeft = this._steps[this._scrollIndex++].end;
+ }
+ }
+
+ _getStepByProgress(progress) {
+ for (let i = 0; i < this._steps.length; i++) {
+ const step = this._steps[i];
+ if (step.start <= progress && step.end >= progress) {
+ return step;
+ }
+ }
+ }
+
+ _updateProgress(progress) {
+ this._progress = progress;
+ this._progressDiv.style.left = progress + 'px';
+ }
+
+ _applyStyle(element, style) {
+ for (let key in style) {
+ element.style[key] = style[key];
+ }
+ }
+
+ destroy() {
+ this._startButton.removeEventListener('click', this._onPlayChange);
+ this._progressDiv.removeEventListener('mousedown', this._onProgressDragStart);
+ this._container.removeChild(this.element);
+ }
+
+}
+
+BMapLib.Timeline = Timeline;
+
+/**
+ * @typedef {Object} TimelineOptions
+ * @property {string[]} times - 时间数组
+ * @property {BMap} map - 地图实例
+ * @property {HTMLElement} [customContainer] - 自定义容器
+ * @property {number} [interval=1000] - 播放间隔,单位毫秒
+ * @property {HTMLElement} [playButton] - 播放按钮
+ * @property {HTMLElement} [pauseButton] - 暂停按钮
+ * @property {string} [className] - 类名
+ * @property {Object} [progressButtonStyle] - 进度按钮样式
+ * @property {Object} [scrollStyle] - 滚动条样式
+ * @property {Object} [timeStyle] - 时间样式
+ * @property {Object} [dividerStyle] - 分割线样式
+ */
+
+/**
+ * @typedef {Object} Step
+ * @property {number} start - 开始px
+ * @property {number} end - 结束px
+ * @property {number} time - 时间
+ * @property {number} index - 索引
+ */
\ No newline at end of file
diff --git a/src/Timeline/Timeline.min.js b/src/Timeline/Timeline.min.js
new file mode 100644
index 0000000..2b8ce12
--- /dev/null
+++ b/src/Timeline/Timeline.min.js
@@ -0,0 +1 @@
+let BMapLib=window.BMapLib||{};const prefix="BMapLib";class Timeline{constructor(t){this.options=t,this.listeners={},this._interval=t.interval||1e3,this._playTimeId=null,this._playStatus="pause",this._startX,this._progressMax=0,this._playIndex=0,this._progress=0,this._steps=[],this._times=t.times||[],this._ctx=document.createElement("canvas").getContext("2d"),this._startScrollIndex=-1,this._scrollIndex=1,this._container=null,this._playButton=t.playButton?t.playButton:document.createTextNode("播放"),this._pauseButton=t.pauseButton?t.pauseButton:document.createTextNode("暂停"),this._initDom(t),this._drawTime()}_initDom(){if(this.element=document.createElement("div"),this.element.className=`${prefix}-timeline`,this.options.className&&(this.element.className+=` ${this.options.className}`),this._startButton=document.createElement("div"),this._startButton.className=`${prefix}-timeline-play`,this._startButton.innerHTML="",this._startButton.appendChild(this._playButton),this.element.appendChild(this._startButton),this._scrollDiv=document.createElement("div"),this._scrollDiv.className=`${prefix}-timeline-main`,this.element.appendChild(this._scrollDiv),this._ul=document.createElement("ul"),this._scrollDiv.appendChild(this._ul),this._progressDiv=document.createElement("div"),this._progressDiv.className=`${prefix}-timeline-progress`,this.options.progressButtonStyle&&this._applyStyle(this._progressDiv,this.options.progressButtonStyle),this._scrollDiv.appendChild(this._progressDiv),this._onPlayChange=this._onPlayChange.bind(this),this._onProgressDragStart=this._onProgressDragStart.bind(this),this._onProgressDrag=this._onProgressDrag.bind(this),this._onProgressDragEnd=this._onProgressDragEnd.bind(this),this._startButton.addEventListener("click",this._onPlayChange),this._progressDiv.addEventListener("mousedown",this._onProgressDragStart),this.options.customContainer)this._container=this.options.customContainer,this.options.customContainer.appendChild(this.element);else{if(!this.options.map)throw new Error("options.map or options.customContainer is required");this._container=this.options.map.getContainer(),this.options.map.getContainer().appendChild(this.element)}this._startX=this._scrollDiv.getBoundingClientRect().left}on(t,s){this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(s)}un(t,s){if(this.listeners[t]){let e=this.listeners[t].indexOf(s);e>-1&&this.listeners[t].splice(e,1)}}dispatchEvent(t){if(this.listeners[t.type])for(let s=0;st){s=e;break}this._startScrollIndex=s}_onProgressDrag(t){const s=t.clientX-this._startX+this._scrollDiv.scrollLeft;s>=0&&s<=this._progressMax&&this._updateProgress(s)}_onProgressDragEnd(t){const s=t.clientX-this._startX,e=this._getStepByProgress(s);e?(this.dispatchEvent({type:"change",time:e.time}),this._playIndex=e.index):this._playIndex=this._steps.length,document.removeEventListener("mousemove",this._onProgressDrag),document.removeEventListener("mouseup",this._onProgressDragEnd)}_onProgressDragStart(t){clearTimeout(this._playTimeId),this._playStatus="pause",this._startButton.innerHTML="",this._startButton.appendChild(this._playButton),document.addEventListener("mousemove",this._onProgressDrag),document.addEventListener("mouseup",this._onProgressDragEnd)}_onPlayChange(){if(this._playTimeId&&clearTimeout(this._playTimeId),this._playIndex>=this._steps.length)this._playIndex=0,this._scrollIndex=1,this._scrollDiv.scrollLeft=0,this._progressDiv.style.left=0;else{let t=this._getStepByProgress(this._progress);this._playIndex=t?t.index:this._steps.length}if("pause"===this._playStatus){this.dispatchEvent({type:"playstart"});const t=()=>{this._playTimeId=setTimeout(t,this._interval);let s=this._steps[this._playIndex++];s&&this._updateProgressByStep(s),this._playIndex>=this._steps.length&&(clearTimeout(this._playTimeId),this._playStatus="pause",this._startButton.innerHTML="",this._startButton.appendChild(this._playButton),this.dispatchEvent({type:"playend"}))};this._playTimeId=setTimeout(t,this._interval),this._playStatus="play",this._startButton.innerHTML="",this._startButton.appendChild(this._pauseButton)}else this._playStatus="pause",this._startButton.innerHTML="",this._startButton.appendChild(this._playButton),this.dispatchEvent({type:"playend"}),clearTimeout(this._playTimeId)}_updateProgressByStep(t){this.dispatchEvent({type:"change",time:t.time}),this._progress=t.end,this._progressDiv.style.left=this._progress-5+"px",this._startScrollIndex>0&&t.end>=this._steps[this._startScrollIndex].end&&(this._scrollDiv.scrollLeft=this._steps[this._scrollIndex++].end)}_getStepByProgress(t){for(let s=0;s=t)return e}}_updateProgress(t){this._progress=t,this._progressDiv.style.left=t+"px"}_applyStyle(t,s){for(let e in s)t.style[e]=s[e]}destroy(){this._startButton.removeEventListener("click",this._onPlayChange),this._progressDiv.removeEventListener("mousedown",this._onProgressDragStart),this._container.removeChild(this.element)}}BMapLib.Timeline=Timeline;
\ No newline at end of file
diff --git a/src/TrackAnimation/TrackAnimation.js b/src/TrackAnimation/TrackAnimation.js
new file mode 100644
index 0000000..ca88d36
--- /dev/null
+++ b/src/TrackAnimation/TrackAnimation.js
@@ -0,0 +1,407 @@
+/**
+ * @fileoverview 百度地图的轨迹播放类,对外开放。
+ * 实现折线轨迹的“逐段增长”动画,并提供播放控制与变速能力。
+ *
+ * @author Baidu Map Api Group
+ */
+
+/**
+ * @namespace BMap的所有library类均放在BMapLib命名空间下
+ */
+var BMapLib = window.BMapLib = BMapLib || {};
+
+(function () {
+ var DEFAULT_DURATION = 10000;
+ var DEFAULT_DELAY = 0;
+ var DEFAULT_OVERALLVIEW = false;
+ var DEFAULT_FOLLOWVIEW = true;
+ var DEFAULT_FOLLOWVIEW_SMOOTHING = 0.18;
+ // 视口跟随触发阈值(0~1)
+ var DEFAULT_FOLLOWVIEW_EDGE_PADDING = 0.28;
+
+ var PLAY = 1;
+ var CANCEL = 2;
+ var PAUSE = 3;
+
+ function now() {
+ return (window.performance && performance.now) ? performance.now() : Date.now();
+ }
+
+ /**
+ * @exports TrackAnimation as BMapLib.TrackAnimation
+ * @constructor
+ * @param {BMap.Map} map 地图实例
+ * @param {BMap.Polyline} polyline 折线实例
+ * @param {Object} opts 配置
+ * {
+ * duration: Number 动画时长(ms)
+ * delay: Number 延迟开始(ms)
+ * overallView: Boolean 是否在结束时 setViewport 展示整条轨迹(默认false)
+ * followView: Boolean 播放中是否跟随当前点移动视口(默认true)
+ * followViewSmoothing: Number 跟随平滑系数(0~1)
+ * followViewEdgePadding: Number 触发跟随的边缘留白比例(0~1),越大越容易触发跟随(默认0.28)
+ * followViewPadding: Number(兼容)同 followViewEdgePadding
+ * onAnimateEnd: Function 动画结束回调
+ * }
+ */
+ BMapLib.TrackAnimation = function (map, polyline, opts) {
+ if (!map || !polyline) {
+ return;
+ }
+ this._map = map;
+ this._polyline = polyline;
+ this._opts = {
+ duration: DEFAULT_DURATION,
+ delay: DEFAULT_DELAY,
+ overallView: DEFAULT_OVERALLVIEW,
+ followView: DEFAULT_FOLLOWVIEW,
+ followViewSmoothing: DEFAULT_FOLLOWVIEW_SMOOTHING,
+ followViewEdgePadding: DEFAULT_FOLLOWVIEW_EDGE_PADDING,
+ onAnimateEnd: null
+ };
+ this._initOpts(opts);
+
+ this._status = CANCEL;
+ this._timer = null;
+ this._delayTimer = null;
+ this._startSeq = 0;
+
+ this._startTime = 0; // 动画起点(基准时间)
+ this._pauseTime = 0; // 暂停累计
+ this._pauseAt = 0; // 暂停时刻
+ this._speedFactor = 1; // 1为基准
+ this._smoothCenterPx = null; // 平滑跟随:像素坐标中心
+
+ this._totalPath = this._polyline.getPath() || [];
+ this._expandPath = [];
+ this._last2Points = [];
+ this._buildExpandedPath();
+ };
+
+ BMapLib.TrackAnimation.prototype._initOpts = function (opts) {
+ if (!opts) return;
+ for (var p in opts) {
+ if (opts.hasOwnProperty(p)) {
+ this._opts[p] = opts[p];
+ }
+ }
+ if (typeof this._opts.duration !== "number" || this._opts.duration <= 0) {
+ this._opts.duration = DEFAULT_DURATION;
+ }
+ if (typeof this._opts.delay !== "number" || this._opts.delay < 0) {
+ this._opts.delay = DEFAULT_DELAY;
+ }
+ if (typeof this._opts.followView !== "boolean") {
+ this._opts.followView = DEFAULT_FOLLOWVIEW;
+ }
+ if (typeof this._opts.followViewSmoothing !== "number" || this._opts.followViewSmoothing <= 0 || this._opts.followViewSmoothing > 1) {
+ this._opts.followViewSmoothing = DEFAULT_FOLLOWVIEW_SMOOTHING;
+ }
+ // 兼容旧字段 followViewPadding
+ if (typeof this._opts.followViewEdgePadding === "undefined" && typeof this._opts.followViewPadding !== "undefined") {
+ this._opts.followViewEdgePadding = this._opts.followViewPadding;
+ }
+ if (typeof this._opts.followViewEdgePadding !== "number" || this._opts.followViewEdgePadding < 0 || this._opts.followViewEdgePadding > 1) {
+ this._opts.followViewEdgePadding = DEFAULT_FOLLOWVIEW_EDGE_PADDING;
+ }
+ };
+
+ /**
+ * 根据时长扩充路径:按段距离分配插值点,保证总点数接近duration/10
+ */
+ BMapLib.TrackAnimation.prototype._buildExpandedPath = function () {
+ var path = this._totalPath || [];
+ if (!path || path.length < 2) {
+ this._expandPath = path ? path.slice(0) : [];
+ return;
+ }
+
+ var totalNum = Math.max(2, Math.floor(this._opts.duration / 10));
+ var length = path.length;
+
+ var distances = [];
+ var totalDistance = 0;
+ for (var i = 1; i < length; i++) {
+ var d = this._map.getDistance(path[i - 1], path[i]);
+ distances.push(d);
+ totalDistance += d;
+ }
+
+ // 极端情况:所有点重合
+ if (!totalDistance || !isFinite(totalDistance)) {
+ this._expandPath = path.slice(0);
+ return;
+ }
+
+ var remaining = totalNum;
+ var expand = [path[0]];
+ for (var j = 1; j < length; j++) {
+ // 最后一段吃掉剩余,减少误差
+ var num;
+ if (j === length - 1) {
+ num = Math.max(1, remaining);
+ } else {
+ num = Math.max(1, Math.round(distances[j - 1] / totalDistance * totalNum));
+ }
+ remaining -= num;
+ expand = expand.concat(this._interpolate(path[j - 1], path[j], num));
+ }
+ this._expandPath = expand;
+ };
+
+ /**
+ * 线性插值
+ * @param {BMap.Point} start
+ * @param {BMap.Point} end
+ * @param {number} num 插值点数量(>=1)
+ */
+ BMapLib.TrackAnimation.prototype._interpolate = function (start, end, num) {
+ var result = [];
+ if (!num || num <= 0) return result;
+ for (var i = 1; i <= num; i++) {
+ var p = new BMap.Point(
+ (end.lng - start.lng) / num * i + start.lng,
+ (end.lat - start.lat) / num * i + start.lat
+ );
+ result.push(p);
+ }
+ return result;
+ };
+
+ /**
+ * 启动动画
+ */
+ BMapLib.TrackAnimation.prototype.start = function () {
+ var me = this;
+ if (!this._polyline || !this._map) return;
+
+ // 刷新路径与插值(允许外部先setPolyline再start)
+ this._totalPath = this._polyline.getPath() || [];
+ this._buildExpandedPath();
+ this.cancel(); // 保证幂等:清理旧状态
+ var seq = ++this._startSeq;
+ this._delayTimer = setTimeout(function () {
+ // 若delay期间被cancel/再次start,则忽略这次启动
+ if (seq !== me._startSeq) {
+ return;
+ }
+ // 轨迹线从第一个点开始
+ me._polyline.setPath(me._expandPath.slice(0, 1));
+ me._status = PLAY;
+ me._startTime = 0;
+ me._pauseTime = 0;
+ me._pauseAt = 0;
+ me._smoothCenterPx = null;
+ me._step(now());
+ }, this._opts.delay);
+ };
+
+ /**
+ * 终止动画并清理
+ */
+ BMapLib.TrackAnimation.prototype.cancel = function () {
+ this._clearRAF();
+ if (this._delayTimer) {
+ clearTimeout(this._delayTimer);
+ this._delayTimer = null;
+ }
+ this._startSeq++;
+ this._status = CANCEL;
+ this._startTime = 0;
+ this._pauseTime = 0;
+ this._pauseAt = 0;
+ this._smoothCenterPx = null;
+ };
+
+ /**
+ * 暂停动画
+ */
+ BMapLib.TrackAnimation.prototype.pause = function () {
+ if (this._status !== PLAY) return;
+ this._clearRAF();
+ this._status = PAUSE;
+ this._pauseAt = now();
+ };
+
+ /**
+ * 继续动画
+ */
+ BMapLib.TrackAnimation.prototype.continue = function () {
+ if (this._status !== PAUSE) return;
+ var t = now();
+ this._pauseTime += (t - this._pauseAt);
+ this._pauseAt = 0;
+ this._status = PLAY;
+ this._step(t);
+ };
+
+ /**
+ * rAF 驱动的逐步绘制
+ */
+ BMapLib.TrackAnimation.prototype._step = function (timestamp) {
+ if (this._status === CANCEL) {
+ this._startTime = 0;
+ return;
+ }
+ if (!this._startTime) {
+ this._startTime = timestamp;
+ }
+ var t = timestamp - this._pauseTime;
+ var percent = (t - this._startTime) / this._opts.duration;
+
+ if (percent < 0) percent = 0;
+ if (percent > 1) percent = 1;
+
+ var end = Math.max(1, Math.round(this._expandPath.length * percent));
+ var currentPath = this._expandPath.slice(0, end);
+ this._last2Points = currentPath.slice(-2);
+ this._polyline.setPath(currentPath);
+
+ // 播放中视口跟随:采用“安全区 + 像素空间平滑”避免抖动
+ if (this._opts.followView && this._map && this._expandPath.length && typeof this._map.pointToPixel === "function" && typeof this._map.pixelToPoint === "function") {
+ var last = currentPath[currentPath.length - 1];
+ if (last) {
+ var size = this._map.getSize && this._map.getSize();
+ if (size && size.width && size.height) {
+ var trackPx = this._map.pointToPixel(last);
+ // 当前视口中心(像素)
+ var centerPt = this._map.getCenter && this._map.getCenter();
+ var centerPx = centerPt ? this._map.pointToPixel(centerPt) : null;
+ if (trackPx && centerPx) {
+ // 安全区:点在中心一定范围内不触发跟随,减少微抖
+ var pad = this._opts.followViewEdgePadding;
+ var halfW = size.width / 2;
+ var halfH = size.height / 2;
+ var safeW = halfW * (1 - pad);
+ var safeH = halfH * (1 - pad);
+ var dx = trackPx.x - centerPx.x;
+ var dy = trackPx.y - centerPx.y;
+
+ if (Math.abs(dx) > safeW || Math.abs(dy) > safeH) {
+ if (!this._smoothCenterPx) {
+ this._smoothCenterPx = {x: centerPx.x, y: centerPx.y};
+ }
+ // 目标中心:把点拉回到安全区边缘(而不是直接居中),更稳
+ var targetCx = centerPx.x;
+ var targetCy = centerPx.y;
+ if (dx > safeW) targetCx += (dx - safeW);
+ if (dx < -safeW) targetCx += (dx + safeW);
+ if (dy > safeH) targetCy += (dy - safeH);
+ if (dy < -safeH) targetCy += (dy + safeH);
+
+ var a = this._opts.followViewSmoothing;
+ this._smoothCenterPx.x += (targetCx - this._smoothCenterPx.x) * a;
+ this._smoothCenterPx.y += (targetCy - this._smoothCenterPx.y) * a;
+
+ var newCenter = this._map.pixelToPoint(new BMap.Pixel(this._smoothCenterPx.x, this._smoothCenterPx.y));
+ if (newCenter && typeof this._map.setCenter === "function") {
+ this._map.setCenter(newCenter);
+ }
+ } else {
+ // 回到安全区内,释放平滑状态,避免拖尾
+ this._smoothCenterPx = null;
+ }
+ }
+ }
+ }
+ }
+
+ if (percent < 1) {
+ this._timer = this._requestFrame(this._step.bind(this));
+ } else {
+ this._startTime = 0;
+ this._pauseTime = 0;
+ this._pauseAt = 0;
+ this._status = CANCEL;
+ // 结束后展示全量轨迹
+ if (this._opts.overallView && this._map && this._totalPath && this._totalPath.length >= 2 && this._map.setViewport) {
+ this._map.setViewport(this._totalPath);
+ }
+ typeof this._opts.onAnimateEnd === "function" && this._opts.onAnimateEnd();
+ }
+ };
+
+ BMapLib.TrackAnimation.prototype._requestFrame = function (cb) {
+ if (window.requestAnimationFrame) {
+ return window.requestAnimationFrame(cb);
+ }
+ return window.setTimeout(function () { cb(now()); }, 16);
+ };
+
+ BMapLib.TrackAnimation.prototype._cancelFrame = function (id) {
+ if (window.cancelAnimationFrame) {
+ window.cancelAnimationFrame(id);
+ return;
+ }
+ window.clearTimeout(id);
+ };
+
+ BMapLib.TrackAnimation.prototype._clearRAF = function () {
+ if (this._timer) {
+ this._cancelFrame(this._timer);
+ this._timer = null;
+ }
+ };
+
+ /**
+ * 设置持续时间(ms)
+ */
+ BMapLib.TrackAnimation.prototype.setDuration = function (duration) {
+ if (typeof duration !== "number" || duration <= 0) return;
+ this._opts.duration = duration;
+ this._buildExpandedPath();
+ };
+
+ BMapLib.TrackAnimation.prototype.getDuration = function () {
+ return this._opts.duration;
+ };
+
+ /**
+ * 设置速度因子(1为基准,>1加速,<1减速)
+ * 通过调整duration,并修正当前进度对应的起点时间,保证不断档
+ */
+ BMapLib.TrackAnimation.prototype.setSpeed = function (speedFactor) {
+ if (typeof speedFactor !== "number" || speedFactor <= 0) return;
+ var oldDuration = this._opts.duration;
+ var newDuration = oldDuration * (1 / speedFactor);
+
+ // 播放中/暂停中需要保持当前进度不跳变
+ if ((this._status === PLAY || this._status === PAUSE) && this._startTime) {
+ var t = (this._status === PAUSE && this._pauseAt) ? this._pauseAt : now();
+ var effective = t - this._pauseTime;
+ var percent = (effective - this._startTime) / oldDuration;
+ if (percent < 0) percent = 0;
+ if (percent > 1) percent = 1;
+ this._startTime = effective - percent * newDuration;
+ }
+
+ this._speedFactor = speedFactor;
+ this.setDuration(newDuration);
+ };
+
+ BMapLib.TrackAnimation.prototype.getSpeed = function () {
+ return this._speedFactor || 1;
+ };
+
+ /**
+ * 更新折线(不自动start)
+ */
+ BMapLib.TrackAnimation.prototype.setPolyline = function (polyline) {
+ if (!polyline) return;
+ this._polyline = polyline;
+ this._totalPath = polyline.getPath() || [];
+ this._buildExpandedPath();
+ };
+
+ BMapLib.TrackAnimation.prototype.getPolyline = function () {
+ return this._polyline;
+ };
+
+ /**
+ * 获取最后两个点(用于外部定位/跟随)
+ */
+ BMapLib.TrackAnimation.prototype.getLastPoint = function () {
+ return this._last2Points.slice(0);
+ };
+})();
+
diff --git a/src/TrackAnimation/TrackAnimation.min.js b/src/TrackAnimation/TrackAnimation.min.js
new file mode 100644
index 0000000..ac3d167
--- /dev/null
+++ b/src/TrackAnimation/TrackAnimation.min.js
@@ -0,0 +1 @@
+var BMapLib=window.BMapLib=BMapLib||{};!function(){var t=!0;function i(){return window.performance&&performance.now?performance.now():Date.now()}BMapLib.TrackAnimation=function(i,e,s){i&&e&&(this._map=i,this._polyline=e,this._opts={duration:1e4,delay:0,overallView:false,followView:t,followViewSmoothing:.18,followViewEdgePadding:.28,onAnimateEnd:null},this._initOpts(s),this._status=2,this._timer=null,this._delayTimer=null,this._startSeq=0,this._startTime=0,this._pauseTime=0,this._pauseAt=0,this._speedFactor=1,this._smoothCenterPx=null,this._totalPath=this._polyline.getPath()||[],this._expandPath=[],this._last2Points=[],this._buildExpandedPath())},BMapLib.TrackAnimation.prototype._initOpts=function(i){if(i){for(var e in i)i.hasOwnProperty(e)&&(this._opts[e]=i[e]);("number"!=typeof this._opts.duration||this._opts.duration<=0)&&(this._opts.duration=1e4),("number"!=typeof this._opts.delay||this._opts.delay<0)&&(this._opts.delay=0),"boolean"!=typeof this._opts.followView&&(this._opts.followView=t),("number"!=typeof this._opts.followViewSmoothing||this._opts.followViewSmoothing<=0||this._opts.followViewSmoothing>1)&&(this._opts.followViewSmoothing=.18),void 0===this._opts.followViewEdgePadding&&void 0!==this._opts.followViewPadding&&(this._opts.followViewEdgePadding=this._opts.followViewPadding),("number"!=typeof this._opts.followViewEdgePadding||this._opts.followViewEdgePadding<0||this._opts.followViewEdgePadding>1)&&(this._opts.followViewEdgePadding=.28)}},BMapLib.TrackAnimation.prototype._buildExpandedPath=function(){var t=this._totalPath||[];if(!t||t.length<2)this._expandPath=t?t.slice(0):[];else{for(var i=Math.max(2,Math.floor(this._opts.duration/10)),e=t.length,s=[],o=0,a=1;a1&&(i=1);var e=Math.max(1,Math.round(this._expandPath.length*i)),s=this._expandPath.slice(0,e);if(this._last2Points=s.slice(-2),this._polyline.setPath(s),this._opts.followView&&this._map&&this._expandPath.length&&"function"==typeof this._map.pointToPixel&&"function"==typeof this._map.pixelToPoint){var o=s[s.length-1];if(o){var a=this._map.getSize&&this._map.getSize();if(a&&a.width&&a.height){var n=this._map.pointToPixel(o),h=this._map.getCenter&&this._map.getCenter(),p=h?this._map.pointToPixel(h):null;if(n&&p){var r=this._opts.followViewEdgePadding,_=a.width/2*(1-r),l=a.height/2*(1-r),m=n.x-p.x,u=n.y-p.y;if(Math.abs(m)>_||Math.abs(u)>l){this._smoothCenterPx||(this._smoothCenterPx={x:p.x,y:p.y});var d=p.x,f=p.y;m>_&&(d+=m-_),m<-_&&(d+=m+_),u>l&&(f+=u-l),u<-l&&(f+=u+l);var c=this._opts.followViewSmoothing;this._smoothCenterPx.x+=(d-this._smoothCenterPx.x)*c,this._smoothCenterPx.y+=(f-this._smoothCenterPx.y)*c;var w=this._map.pixelToPoint(new BMap.Pixel(this._smoothCenterPx.x,this._smoothCenterPx.y));w&&"function"==typeof this._map.setCenter&&this._map.setCenter(w)}else this._smoothCenterPx=null}}}}i<1?this._timer=this._requestFrame(this._step.bind(this)):(this._startTime=0,this._pauseTime=0,this._pauseAt=0,this._status=2,this._opts.overallView&&this._map&&this._totalPath&&this._totalPath.length>=2&&this._map.setViewport&&this._map.setViewport(this._totalPath),"function"==typeof this._opts.onAnimateEnd&&this._opts.onAnimateEnd())}else this._startTime=0},BMapLib.TrackAnimation.prototype._requestFrame=function(t){return window.requestAnimationFrame?window.requestAnimationFrame(t):window.setTimeout(function(){t(i())},16)},BMapLib.TrackAnimation.prototype._cancelFrame=function(t){window.cancelAnimationFrame?window.cancelAnimationFrame(t):window.clearTimeout(t)},BMapLib.TrackAnimation.prototype._clearRAF=function(){this._timer&&(this._cancelFrame(this._timer),this._timer=null)},BMapLib.TrackAnimation.prototype.setDuration=function(t){"number"!=typeof t||t<=0||(this._opts.duration=t,this._buildExpandedPath())},BMapLib.TrackAnimation.prototype.getDuration=function(){return this._opts.duration},BMapLib.TrackAnimation.prototype.setSpeed=function(t){if(!("number"!=typeof t||t<=0)){var e=this._opts.duration,s=e*(1/t);if((1===this._status||3===this._status)&&this._startTime){var o=(3===this._status&&this._pauseAt?this._pauseAt:i())-this._pauseTime,a=(o-this._startTime)/e;a<0&&(a=0),a>1&&(a=1),this._startTime=o-a*s}this._speedFactor=t,this.setDuration(s)}},BMapLib.TrackAnimation.prototype.getSpeed=function(){return this._speedFactor||1},BMapLib.TrackAnimation.prototype.setPolyline=function(t){t&&(this._polyline=t,this._totalPath=t.getPath()||[],this._buildExpandedPath())},BMapLib.TrackAnimation.prototype.getPolyline=function(){return this._polyline},BMapLib.TrackAnimation.prototype.getLastPoint=function(){return this._last2Points.slice(0)}}();
\ No newline at end of file