-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(core): GlobeView pointer-anchored zoom (wheel + transitions) #10307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
69d7ebf
e60090a
0e9285b
f7a0843
aede89b
05224b8
4ad99ed
1c01414
5150bf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,12 @@ | |
| import type {MapStateInternal} from './map-controller'; | ||
| import {mod} from '../utils/math-utils'; | ||
| import LinearInterpolator from '../transitions/linear-interpolator'; | ||
| import {zoomAdjust, GLOBE_RADIUS} from '../viewports/globe-viewport'; | ||
| import type Viewport from '../viewports/viewport'; | ||
| import GlobeViewport, { | ||
| zoomAdjust, | ||
| GLOBE_RADIUS, | ||
| GLOBE_ZOOM_ANCHOR_MAX_DISTANCE_RATIO | ||
| } from '../viewports/globe-viewport'; | ||
| import { | ||
| Globe, | ||
| type CameraFrame, | ||
|
|
@@ -35,26 +40,31 @@ | |
| return radians * RADIANS_TO_DEGREES; | ||
| } | ||
|
|
||
| type GlobeZoomAround = 'center' | 'pointer'; | ||
|
|
||
| type GlobeStateInternal = MapStateInternal & { | ||
| startPanPos?: [number, number]; | ||
| startPanCameraFrame?: CameraFrame; | ||
| startPanAngularRate?: number; | ||
| /** When true, bearing is held fixed during pan (north stays up) */ | ||
| startPanLockBearing?: boolean; | ||
| zoomAround?: GlobeZoomAround; | ||
| }; | ||
|
|
||
| class GlobeState extends MapState { | ||
| constructor( | ||
| options: MapStateProps & | ||
| GlobeStateInternal & { | ||
| makeViewport: (props: Record<string, any>) => any; | ||
| zoomAround?: GlobeZoomAround; | ||
| } | ||
| ) { | ||
| const { | ||
| startPanPos, | ||
| startPanCameraFrame, | ||
| startPanAngularRate, | ||
| startPanLockBearing, | ||
| zoomAround, | ||
| ...mapStateOptions | ||
| } = options; | ||
| mapStateOptions.normalize = false; | ||
|
|
@@ -65,6 +75,7 @@ | |
| if (startPanCameraFrame !== undefined) s.startPanCameraFrame = startPanCameraFrame; | ||
| if (startPanAngularRate !== undefined) s.startPanAngularRate = startPanAngularRate; | ||
| if (startPanLockBearing !== undefined) s.startPanLockBearing = startPanLockBearing; | ||
| if (zoomAround !== undefined) s.zoomAround = zoomAround; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was investigating the intermittent issue where pointer-anchored zoom stops working after hot reload. I think this may help: - if (zoomAround !== undefined) s.zoomAround = zoomAround;
+ s.zoomAround = zoomAround || 'center';The problem might be that To reproduce: set Unlike the other fields guarded by |
||
| } | ||
|
|
||
| panStart({pos}: {pos: [number, number]}): GlobeState { | ||
|
|
@@ -142,10 +153,59 @@ | |
| }) as GlobeState; | ||
| } | ||
|
|
||
| zoom({scale}: {scale: number}): MapState { | ||
| const startZoom = this.getState().startZoom || this.getViewportProps().zoom; | ||
| const zoom = startZoom + Math.log2(scale); | ||
| return this._getUpdatedState({zoom}); | ||
| zoomStart({pos}: {pos: [number, number]}): GlobeState { | ||
| const startZoomLngLat = this._shouldZoomAroundPointer() | ||
| ? this._unprojectZoomAnchor(pos) | ||
| : undefined; | ||
|
|
||
| return this._getUpdatedState({ | ||
| startZoomLngLat, | ||
| startZoom: this.getViewportProps().zoom | ||
| }) as GlobeState; | ||
| } | ||
|
|
||
| zoom({ | ||
| pos, | ||
| startPos, | ||
| scale | ||
| }: { | ||
| pos: [number, number]; | ||
| startPos?: [number, number]; | ||
| scale: number; | ||
| }): MapState { | ||
| const state = this.getState(); | ||
| const {startZoom} = state; | ||
| const initialStartZoomLngLat = state.startZoomLngLat as [number, number] | undefined; | ||
| const hasZoomStart = startZoom !== null && startZoom !== undefined; | ||
| const startZoomValue = hasZoomStart ? startZoom : this.getViewportProps().zoom; | ||
| const zoom = this._constrainZoom(startZoomValue + Math.log2(scale)); | ||
|
|
||
| if (!this._shouldZoomAroundPointer()) { | ||
| return this._getUpdatedState({zoom}); | ||
| } | ||
|
|
||
| const startZoomLngLat = | ||
| initialStartZoomLngLat ?? | ||
| this._unprojectZoomAnchor(startPos) ?? | ||
| this._unprojectZoomAnchor(pos); | ||
|
|
||
| if (!startZoomLngLat) { | ||
| return this._getUpdatedState({zoom}); | ||
| } | ||
|
|
||
| const zoomedViewport = this.makeViewport({...this.getViewportProps(), zoom}); | ||
| return this._getUpdatedState({ | ||
| zoom, | ||
| ...(hasZoomStart && !initialStartZoomLngLat ? {startZoomLngLat} : {}), | ||
| ...this._panByZoomAnchor(zoomedViewport, startZoomLngLat, pos) | ||
| }); | ||
| } | ||
|
|
||
| zoomEnd(): GlobeState { | ||
| return this._getUpdatedState({ | ||
| startZoomLngLat: null, | ||
| startZoom: null | ||
| }) as GlobeState; | ||
| } | ||
|
|
||
| _panFromCenter(offset: [number, number]): GlobeState { | ||
|
|
@@ -242,6 +302,37 @@ | |
| const zoomAdjustment = zoomAdjust(props.latitude, true) - zoomAdjust(0, true); | ||
| return clamp(zoom, minZoom + zoomAdjustment, maxZoom + zoomAdjustment); | ||
| } | ||
|
|
||
| private _unprojectZoomAnchor(pos?: [number, number]): [number, number] | undefined { | ||
| if (!pos) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const viewport = this.makeViewport(this.getViewportProps()); | ||
| if ( | ||
| viewport instanceof GlobeViewport && | ||
| !viewport.isPointOnGlobe(pos, {maxDistanceRatio: GLOBE_ZOOM_ANCHOR_MAX_DISTANCE_RATIO}) | ||
| ) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const lngLat = viewport.unproject(pos); | ||
| return [lngLat[0], lngLat[1]]; | ||
| } | ||
|
|
||
| private _panByZoomAnchor( | ||
| viewport: Viewport, | ||
| anchorLngLat: [number, number], | ||
| pixel: [number, number] | ||
| ): Record<string, any> { | ||
| return viewport instanceof GlobeViewport | ||
| ? viewport.panByGlobeAnchor(anchorLngLat, pixel) | ||
| : viewport.panByPosition(anchorLngLat, pixel); | ||
| } | ||
|
|
||
| private _shouldZoomAroundPointer(): boolean { | ||
| return (this.getState() as GlobeStateInternal).zoomAround === 'pointer'; | ||
| } | ||
| } | ||
|
|
||
| export default class GlobeController extends Controller<MapState> { | ||
|
|
@@ -262,6 +353,15 @@ | |
| // Ring buffer tracking globe position during pan for inertia velocity | ||
| private _panHistory: Array<{longitude: number; latitude: number; timestamp: number}> = []; | ||
|
|
||
| protected _getTransitionProps(opts?: any) { | ||
| if (opts?.around && this.props.zoomAround !== 'pointer') { | ||
| const centerZoomOpts = {...opts}; | ||
| delete centerZoomOpts.around; | ||
| return super._getTransitionProps(centerZoomOpts); | ||
| } | ||
| return super._getTransitionProps(opts); | ||
| } | ||
|
|
||
| protected _onPanStart(event: MjolnirGestureEvent): boolean { | ||
| this._panHistory = []; | ||
| return super._onPanStart(event); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe
zoomAround: pointeris default behavior for most controllers - not sure if this is necessary.