diff --git a/README.md b/README.md index a336d81..f511eb3 100644 --- a/README.md +++ b/README.md @@ -238,3 +238,54 @@ 类参考 + + +轨迹动画(TrackAnimation) +--------- + + + + +
+ + +  TrackAnimation类提供视角轨迹动画展示效果。

+轨迹动画示例
+源码   +压缩源码   +类参考 +
+ +时间轴(Timeline) +--------- + + + + +
+ + +  Timeline 提供时间轴控件能力,可播放/暂停,方便结合地图做时序数据展示。

+时间轴示例
+源码   +压缩源码   +样式   +类参考 +
+ +卷帘对比(Swipe) +--------- + + + + +
+ + +  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