Skip to content
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

Sync continuously-updating interaction handlers with render loop #6005

Merged
merged 3 commits into from
Jan 19, 2018

Conversation

anandthakker
Copy link
Contributor

@anandthakker anandthakker commented Jan 17, 2018

Closes #5390

Quoting from that issue:

the DragPan handler updates the camera directly and fires a move event. Updates to the DOM that happen in move listeners happen synchronously, but the map renderer doesn't update until the next animation frame.

This PR aims to solve this issue by having the DragPan handler (and the other continuously-updating handlers) cede control to the camera, which in turn is driven by the render loop.

I think it's also a step towards further refactoring of camera of Camera that eliminates some of the complicated internal state, simplifies/clarifies move-related event firing, etc.

Launch Checklist

@anandthakker
Copy link
Contributor Author

@mourner and others on @mapbox/gl-core, could you give this a quick look to validate the approach, before I propagate it to the other handlers (DragRotate, etc.)?

src/ui/camera.js Outdated
*
* @private
*/
_startAnimation(onFrame: (Transform) => void,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marked with _ because, since Camera gets mixed directly into Map, I didn't want to make it "public". Eventually, this should be a public method on an internal Camera class that's separate from Map. Similarly, a refactor like that would allow for a better naming of _updateCamera

src/ui/camera.js Outdated
*/
_updateCamera() {
if (this._onFrame) {
this._onFrame(this.transform);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later, we could also consider a design where, rather than mutating the Transform directly, onFrame just returns CameraOptions. Consolidating the responsibility for actual Transform manipulation into one place would slightly simplify the code in easeTo, flyTo, handlers, etc.

Copy link
Contributor

@mollymerp mollymerp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach seems solid to me (albeit I have little to no experience working with the camera animations / render loop codebase) – makes sense to me that the camera would be the single source of truth for animations and animation-related events. It would be nice if any new state tracking could be contained within the camera and/or eliminated, but that may not be possible right now.

}, { originalEvent: e });
_onUp(e: MouseEvent | TouchEvent | FocusEvent) {
if (!this.isActive()) return;
this._map.stop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to delete this._previousPos here to avoid the map stuttering back to the end of the last dragpan on a subsequent dragpan.

Copy link
Member

@mourner mourner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a good approach, with a few exceptions I'm worried about:

  • startAnimation term feels confusing in context of updating transform once in a single frame (since there no animation happening, and it's not clear when the stop handler happens too in this context). Maybe we could rename/refactor this a bit with clearer terms? E.g. naming this as _setCameraUpdateHandler, and finding a different place for the stop logic?
  • You're introducing a lot of closures that get created on every frame. Eliminating them would ensure the code is as fast as it was, and it would also improve code clarity.

BTW there's something similar going on in Leaflet — it schedules camera updates throw requestAnimFrame rather than syncrhonously updating DOM.

@anandthakker
Copy link
Contributor Author

@mourner removed closures and attempted to clarify the code a bit more. I still lean towards calling it _startAnimation, even if it's slightly stretching the idea of 'animation'... but I don't feel strongly about it / happy to change it.

This is ready for a real review now @mollymerp @mourner

Copy link
Member

@mourner mourner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, pending a green build. Also, would be nice to move down/up/move methods to their former positions so that there's less noise in the diff.

* Since we `_update` on move, and that already triggers a `_rerender()`, we don't also need to set a separate 'move' listener that calls `_rerender()`
* Since `_updateEasing()` triggers a `move` event, which in turn schedules a subsequent frame, we don't _also_ need to check `isEasing()` at the end of `render`.
@anandthakker anandthakker force-pushed the event-render-sync branch 2 times, most recently from 9c46380 to e4bb33b Compare January 19, 2018 03:06
Anand Thakker added 2 commits January 18, 2018 22:20
This refactors `Camera#ease()`, which assumed an animation with a
fixed end time, in terms of `Camera#_startAnimation()` (and
`Camera#stop()`), which doesn't. The advantage of this slight
generalization is that `_startAnimation()` can then also be used by
continuously-updating interaction handlers (`DragPan`, etc.)
Instead of updating the transform directly within the mousemove handler,
we cede control to the render loop by doing our transform updates in the
callback we pass to `Camera#_startAnimation`.  This way, we synchronously
update the transform, render the map, and fire the `move` event (and
thus trigger any listeners that might perform DOM updates).
@anandthakker anandthakker merged commit 0f4441b into master Jan 19, 2018
anandthakker pushed a commit that referenced this pull request Jan 26, 2018
Closes #6063

Caused by #6005 because,
after that change, the drag handlers relied on the `move` event to
trigger a map rerender to pick up the next batch of mouse movements.

This worked fine while dragging was in progress, but after the user
paused and the render frame triggered by the last `move` event was
complete, there was nothing to re-initiate the render loop once the
mouse moved again.
anandthakker added a commit that referenced this pull request Jan 27, 2018
Closes #6063

Caused by #6005 because,
after that change, the drag handlers relied on the `move` event to
trigger a map rerender to pick up the next batch of mouse movements.

This worked fine while dragging was in progress, but after the user
paused and the render frame triggered by the last `move` event was
complete, there was nothing to re-initiate the render loop once the
mouse moved again.
anandthakker added a commit that referenced this pull request Jan 29, 2018
Closes #6063

Caused by #6005 because,
after that change, the drag handlers relied on the `move` event to
trigger a map rerender to pick up the next batch of mouse movements.

This worked fine while dragging was in progress, but after the user
paused and the render frame triggered by the last `move` event was
complete, there was nothing to re-initiate the render loop once the
mouse moved again.
pathmapper pushed a commit to pathmapper/mapbox-gl-js that referenced this pull request Feb 13, 2018
…box#6073)

Closes mapbox#6063

Caused by mapbox#6005 because,
after that change, the drag handlers relied on the `move` event to
trigger a map rerender to pick up the next batch of mouse movements.

This worked fine while dragging was in progress, but after the user
paused and the render frame triggered by the last `move` event was
complete, there was nothing to re-initiate the render loop once the
mouse moved again.
@jfirebaugh jfirebaugh deleted the event-render-sync branch February 13, 2018 21:19
pathmapper pushed a commit to pathmapper/mapbox-gl-js that referenced this pull request Feb 13, 2018
…box#6073)

Closes mapbox#6063

Caused by mapbox#6005 because,
after that change, the drag handlers relied on the `move` event to
trigger a map rerender to pick up the next batch of mouse movements.

This worked fine while dragging was in progress, but after the user
paused and the render frame triggered by the last `move` event was
complete, there was nothing to re-initiate the render loop once the
mouse moved again.
@@ -1528,7 +1525,7 @@ class Map extends Camera {
// Even though `_styleDirty` and `_sourcesDirty` are reset in this
// method, synchronous events fired during Style#update or
// Style#_updateSources could have caused them to be set again.
if (this._sourcesDirty || this._repaint || this._styleDirty || this._placementDirty || this.isEasing()) {
if (this._sourcesDirty || this._repaint || this._styleDirty || this._placementDirty) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're planning to fully convert the handlers to use render-loop-driven camera, then we should:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants