Skip to content

Commit 9c62b8b

Browse files
authored
Globe white flash fix (maplibre#4845)
* Fix white frame when transitioning to globe projection * Refactor globeness animation * Update changelog * Add unit test that checks that globe transform and globe projection have their globe state synchronized * Shorter sleeps in globe transform tests
1 parent 76edc77 commit 9c62b8b

File tree

5 files changed

+43
-36
lines changed

5 files changed

+43
-36
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
### 🐞 Bug fixes
77
- Fix text not being hidden behind the globe when overlap mode was set to `always` ([#4802](https://github.com/maplibre/maplibre-gl-js/issues/4802))
8+
- Fix a single white frame being displayed when the map internally transitions from mercator to globe projection ([#4816](https://github.com/maplibre/maplibre-gl-js/issues/4816))
89
- _...Add new stuff here..._
910

1011
## 5.0.0-pre.2

src/geo/projection/globe.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export const globeConstants = {
1717
* Used for globe rendering.
1818
*/
1919
globeTransitionTimeSeconds: 0.5,
20-
zoomTransitionTimeSeconds: 0.5,
2120
maxGlobeZoom: 12.0,
2221
errorTransitionTimeSeconds: 0.5
2322
};

src/geo/projection/globe_transform.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {GlobeProjection} from './globe';
1+
import {globeConstants, GlobeProjection} from './globe';
22
import {EXTENT} from '../../data/extent';
33
import Point from '@mapbox/point-geometry';
44
import {LngLat} from '../lng_lat';
@@ -34,6 +34,9 @@ function createGlobeTransform(globeProjection: GlobeProjection) {
3434

3535
describe('GlobeTransform', () => {
3636
const globeProjectionMock = getGlobeProjectionMock();
37+
// Force faster animations so we can use shorter sleeps when testing them
38+
globeConstants.globeTransitionTimeSeconds = 0.1;
39+
globeConstants.errorTransitionTimeSeconds = 0.1;
3740

3841
describe('getProjectionData', () => {
3942
const globeTransform = createGlobeTransform(globeProjectionMock);
@@ -447,12 +450,12 @@ describe('GlobeTransform', () => {
447450
globeTransform.newFrameUpdate();
448451
globeTransform.setGlobeViewAllowed(false);
449452

450-
await sleep(20);
453+
await sleep(10);
451454
globeTransform.newFrameUpdate();
452455
expect(globeTransform.getGlobeViewAllowed()).toBe(false);
453456
expect(globeTransform.isGlobeRendering).toBe(true);
454457

455-
await sleep(1000);
458+
await sleep(150);
456459
globeTransform.newFrameUpdate();
457460
expect(globeTransform.getGlobeViewAllowed()).toBe(false);
458461
expect(globeTransform.isGlobeRendering).toBe(false);
@@ -463,7 +466,7 @@ describe('GlobeTransform', () => {
463466
globeTransform.newFrameUpdate();
464467
globeTransform.setGlobeViewAllowed(false, false);
465468

466-
await sleep(20);
469+
await sleep(10);
467470
globeTransform.newFrameUpdate();
468471
expect(globeTransform.getGlobeViewAllowed()).toBe(false);
469472
expect(globeTransform.isGlobeRendering).toBe(false);
@@ -731,4 +734,26 @@ describe('GlobeTransform', () => {
731734
]);
732735
});
733736
});
737+
738+
test('transform and projection instance are synchronized properly', async () => {
739+
const projectionMock = getGlobeProjectionMock();
740+
const globeTransform = createGlobeTransform(projectionMock);
741+
// projectionMock.useGlobeRendering and globeTransform.isGlobeRendering must have the same value
742+
expect(projectionMock.useGlobeRendering).toBe(true);
743+
expect(globeTransform.isGlobeRendering).toBe(projectionMock.useGlobeRendering);
744+
globeTransform.setGlobeViewAllowed(false);
745+
globeTransform.newFrameUpdate();
746+
expect(projectionMock.useGlobeRendering).toBe(false);
747+
expect(globeTransform.isGlobeRendering).toBe(projectionMock.useGlobeRendering);
748+
749+
await sleep(150);
750+
globeTransform.setGlobeViewAllowed(true);
751+
globeTransform.newFrameUpdate();
752+
expect(projectionMock.useGlobeRendering).toBe(false);
753+
expect(globeTransform.isGlobeRendering).toBe(projectionMock.useGlobeRendering);
754+
await sleep(10);
755+
globeTransform.newFrameUpdate();
756+
expect(projectionMock.useGlobeRendering).toBe(true);
757+
expect(globeTransform.isGlobeRendering).toBe(projectionMock.useGlobeRendering);
758+
});
734759
});

src/geo/projection/globe_transform.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,17 @@ export class GlobeTransform implements ITransform {
213213
// Transition handling
214214
private _lastGlobeStateEnabled: boolean = true;
215215

216-
private _lastLargeZoomStateChange: number = -1000.0;
217-
private _lastLargeZoomState: boolean = false;
218-
219216
/**
220217
* Stores when {@link newFrameUpdate} was last called.
221218
* Serves as a unified clock for globe (instead of each function using a slightly different value from `browser.now()`).
222219
*/
223-
private _lastUpdateTime = browser.now();
220+
private _lastUpdateTimeSeconds = browser.now() / 1000.0;
224221
/**
225222
* Stores when switch from globe to mercator or back last occurred, for animation purposes.
226223
* This switch can be caused either by the map passing the threshold zoom level,
227224
* or by {@link setGlobeViewAllowed} being called.
228225
*/
229-
private _lastGlobeChangeTime: number = browser.now() - 10_000; // Ten seconds before transform creation
226+
private _lastGlobeChangeTimeSeconds: number = browser.now() / 1000 - 10; // Ten seconds before transform creation
230227

231228
private _skipNextAnimation: boolean = true;
232229

@@ -351,19 +348,18 @@ export class GlobeTransform implements ITransform {
351348
this._skipNextAnimation = true;
352349
}
353350
this._globeProjectionAllowed = allow;
354-
this._lastGlobeChangeTime = this._lastUpdateTime;
351+
this._lastGlobeChangeTimeSeconds = this._lastUpdateTimeSeconds;
355352
}
356353

357354
/**
358355
* Should be called at the beginning of every frame to synchronize the transform with the underlying projection.
359356
*/
360357
newFrameUpdate(): TransformUpdateResult {
361-
this._updateErrorCorrectionValue();
362-
363-
this._lastUpdateTime = browser.now();
358+
this._lastUpdateTimeSeconds = browser.now() / 1000.0;
364359
const oldGlobeRendering = this.isGlobeRendering;
365360
this._globeness = this._computeGlobenessAnimation();
366-
361+
// Everything below this comment must happen AFTER globeness update
362+
this._updateErrorCorrectionValue();
367363
this._calcMatrices();
368364

369365
if (oldGlobeRendering === this.isGlobeRendering) {
@@ -400,34 +396,25 @@ export class GlobeTransform implements ITransform {
400396
*/
401397
private _computeGlobenessAnimation(): number {
402398
// Update globe transition animation
403-
const globeState = this._globeProjectionAllowed;
404-
const currentTime = this._lastUpdateTime;
399+
const globeState = this._globeProjectionAllowed && this.zoom < globeConstants.maxGlobeZoom;
400+
const currentTimeSeconds = this._lastUpdateTimeSeconds;
405401
if (globeState !== this._lastGlobeStateEnabled) {
406-
this._lastGlobeChangeTime = currentTime;
402+
this._lastGlobeChangeTimeSeconds = currentTimeSeconds;
407403
this._lastGlobeStateEnabled = globeState;
408404
}
409405

410406
const oldGlobeness = this._globeness;
411407

412408
// Transition parameter, where 0 is the start and 1 is end.
413-
const globeTransition = Math.min(Math.max((currentTime - this._lastGlobeChangeTime) / 1000.0 / globeConstants.globeTransitionTimeSeconds, 0.0), 1.0);
409+
const globeTransition = Math.min(Math.max((currentTimeSeconds - this._lastGlobeChangeTimeSeconds) / globeConstants.globeTransitionTimeSeconds, 0.0), 1.0);
414410
let newGlobeness = globeState ? globeTransition : (1.0 - globeTransition);
415411

416412
if (this._skipNextAnimation) {
417413
newGlobeness = globeState ? 1.0 : 0.0;
418-
this._lastGlobeChangeTime = currentTime - globeConstants.globeTransitionTimeSeconds * 1000.0 * 2.0;
414+
this._lastGlobeChangeTimeSeconds = currentTimeSeconds - globeConstants.globeTransitionTimeSeconds * 2.0;
419415
this._skipNextAnimation = false;
420416
}
421417

422-
// Update globe zoom transition
423-
const currentZoomState = this.zoom >= globeConstants.maxGlobeZoom;
424-
if (currentZoomState !== this._lastLargeZoomState) {
425-
this._lastLargeZoomState = currentZoomState;
426-
this._lastLargeZoomStateChange = currentTime;
427-
}
428-
const zoomTransition = Math.min(Math.max((currentTime - this._lastLargeZoomStateChange) / 1000.0 / globeConstants.zoomTransitionTimeSeconds, 0.0), 1.0);
429-
const zoomGlobenessBound = currentZoomState ? (1.0 - zoomTransition) : zoomTransition;
430-
newGlobeness = Math.min(newGlobeness, zoomGlobenessBound);
431418
newGlobeness = easeCubicInOut(newGlobeness); // Smooth animation
432419

433420
if (oldGlobeness !== newGlobeness) {
@@ -443,7 +430,7 @@ export class GlobeTransform implements ITransform {
443430

444431
isRenderingDirty(): boolean {
445432
// Globe transition
446-
return (this._lastUpdateTime - this._lastGlobeChangeTime) / 1000.0 < (Math.max(globeConstants.globeTransitionTimeSeconds, globeConstants.zoomTransitionTimeSeconds));
433+
return (this._lastUpdateTimeSeconds - this._lastGlobeChangeTimeSeconds) < globeConstants.globeTransitionTimeSeconds;
447434
}
448435

449436
getProjectionData(overscaledTileID: OverscaledTileID, aligned?: boolean, ignoreTerrainMatrix?: boolean): ProjectionData {

src/util/test/util.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,7 @@ export function getGlobeProjectionMock(): GlobeProjection {
230230
get useGlobeControls(): boolean {
231231
return true;
232232
},
233-
get useGlobeRendering(): boolean {
234-
return true;
235-
},
236-
set useGlobeRendering(_value: boolean) {
237-
// do not set
238-
},
233+
useGlobeRendering: true,
239234
latitudeErrorCorrectionRadians: 0,
240235
errorQueryLatitudeDegrees: 0,
241236
} as GlobeProjection;

0 commit comments

Comments
 (0)