Skip to content

Commit

Permalink
feat(core): add inertia to drag-to-pan interaction (#240)
Browse files Browse the repository at this point in the history
* feat: drag inertia PoC, still work in progress

* feat: cancel inertia on mouse down event

* chore: improvements

* feat: improve the detection of mouse deceleration

* chore: improve cooperation with picking, wheeling, etc

* chore: adjust half life
  • Loading branch information
tuner committed Feb 20, 2024
1 parent 4c4aabc commit 8b00907
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 25 deletions.
5 changes: 3 additions & 2 deletions packages/app/src/sampleView/sampleView.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { watch } from "../state/watch.js";
import { createSelector } from "@reduxjs/toolkit";
import { LocationManager, getSampleLocationAt } from "./locationManager.js";
import { contextMenu, DIVIDER } from "../utils/ui/contextMenu.js";
import interactionToZoom from "@genome-spy/core/view/zoom.js";
import { interactionToZoom } from "@genome-spy/core/view/zoom.js";
import Rectangle from "@genome-spy/core/view/layout/rectangle.js";
import { faArrowsAltV, faXmark } from "@fortawesome/free-solid-svg-icons";
import {
Expand Down Expand Up @@ -765,7 +765,8 @@ export default class SampleView extends ContainerView {
this.#gridChild.view,
zoomEvent
),
this.context.getCurrentHover()
this.context.getCurrentHover(),
this.context.animator
);
}

Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/genomeSpy.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { invalidatePrefix } from "./utils/propertyCacher.js";
import { VIEW_ROOT_NAME, ViewFactory } from "./view/viewFactory.js";
import { reconfigureScales } from "./view/scaleResolution.js";
import createBindingInputs from "./utils/inputBinding.js";
import { isStillZooming } from "./view/zoom.js";

/**
* Events that are broadcasted to all views.
Expand Down Expand Up @@ -687,8 +688,10 @@ export default class GenomeSpy {
this.tooltip.handleMouseMove(event);
this._tooltipUpdateRequested = false;

if (event.buttons == 0) {
// Disable during dragging
// Disable picking during dragging. Also postpone picking until
// the user has stopped zooming as reading pixels from the
// picking buffer is slow and ruins smooth animations.
if (event.buttons == 0 && !isStillZooming()) {
this.renderPickingFramebuffer();
this._handlePicking(point.x, point.y);
}
Expand All @@ -711,7 +714,11 @@ export default class GenomeSpy {
this._wheelInertia.cancel();
}

if (event.type == "mousedown" || event.type == "mouseup") {
if (
(event.type == "mousedown" || event.type == "mouseup") &&
!isStillZooming()
) {
// Actually, only needed when clicking on a mark
this.renderPickingFramebuffer();
} else if (event.type == "wheel") {
lastWheelEvent = now;
Expand Down
24 changes: 15 additions & 9 deletions packages/core/src/utils/animator.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default class Animator {
* @param {number} halfLife Time until half of the value is reached, in milliseconds
* @param {number} stopAt Stop animation when the value is within this distance from the target
* @param {number} [initialValue] Initial value
* @returns {(target: number) => void} Function that activates the transition with a new target value
* @returns {((target: number) => void) & { stop: () => void}} Function that activates the transition with a new target value
*/
export function makeLerpSmoother(
animator,
Expand All @@ -113,15 +113,13 @@ export function makeLerpSmoother(
* @param {number} [timestamp]
*/
function smoothUpdate(timestamp) {
timestamp ??= +document.timeline.currentTime;
if (settled) {
return;
}

// If settled, the animation loop may have been stopped, so we need to
// wait until the next frame to get a proper time delta.
const tD = settled ? 0 : timestamp - lastTimeStamp;
const tD = timestamp - lastTimeStamp;
lastTimeStamp = timestamp;

settled = false;

// Lerp smoothing: https://twitter.com/FreyaHolmer/status/1757836988495847568
current = target + (current - target) * Math.pow(2, -tD / halfLife);

Expand All @@ -140,10 +138,18 @@ export function makeLerpSmoother(
/**
* @param {number} value
*/
return function setTarget(value) {
function setTarget(value) {
target = value;
if (settled) {
smoothUpdate();
settled = false;
lastTimeStamp = +document.timeline.currentTime;
smoothUpdate(lastTimeStamp);
}
}

setTarget.stop = () => {
settled = true;
};

return setTarget;
}
43 changes: 43 additions & 0 deletions packages/core/src/utils/ringBuffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @template T
*/
export default class RingBuffer {
/** @type {T[]} */
#buffer;

#index = 0;

#length = 0;

/**
* @param {number} size
*/
constructor(size) {
this.#buffer = new Array(size);
}

/** @param {T} value */
push(value) {
this.#buffer[this.#index] = value;
this.#index = (this.#index + 1) % this.size;
this.#length = Math.min(this.#length + 1, this.size);
}

/**
* @returns {T[]}
*/
get() {
const b = this.#buffer;
return this.#length < this.size
? b.slice(0, this.#length)
: b.slice(this.#index, this.size).concat(b.slice(0, this.#index));
}

get size() {
return this.#buffer.length;
}

get length() {
return this.#length;
}
}
39 changes: 39 additions & 0 deletions packages/core/src/utils/ringBuffer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, test } from "vitest";
import RingBuffer from "./ringBuffer.js";

describe("ringBuffer", () => {
test("Empty buffer", () => {
const buffer = new RingBuffer(10);
expect(buffer.length).toBe(0);
expect(buffer.get()).toEqual([]);
});

test("Partially filled buffer", () => {
const buffer = new RingBuffer(10);
buffer.push(1);
buffer.push(2);
buffer.push(3);
expect(buffer.length).toBe(3);
expect(buffer.get()).toEqual([1, 2, 3]);
});

test("Full buffer", () => {
const buffer = new RingBuffer(3);
buffer.push(1);
buffer.push(2);
buffer.push(3);
expect(buffer.length).toBe(3);
expect(buffer.get()).toEqual([1, 2, 3]);
});

test("Overfilled buffer", () => {
const buffer = new RingBuffer(3);
buffer.push(1);
buffer.push(2);
buffer.push(3);
buffer.push(4);
buffer.push(5);
expect(buffer.length).toBe(3);
expect(buffer.get()).toEqual([3, 4, 5]);
});
});
5 changes: 3 additions & 2 deletions packages/core/src/view/gridView.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import ContainerView from "./containerView.js";
import LayerView from "./layerView.js";
import createTitle from "./title.js";
import UnitView from "./unitView.js";
import interactionToZoom from "./zoom.js";
import { interactionToZoom } from "./zoom.js";
import clamp from "../utils/clamp.js";
import { makeLerpSmoother } from "../utils/animator.js";

Expand Down Expand Up @@ -712,7 +712,8 @@ export default class GridView extends ContainerView {
pointedChild.view,
zoomEvent
),
this.context.getCurrentHover()
this.context.getCurrentHover(),
this.context.animator
);
}
}
Expand Down
37 changes: 36 additions & 1 deletion packages/core/src/view/layout/point.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
/*
* Hmm. This looks quite a bit like a two-dimensional vector.
* Maybe we should use a vector instead?
*/
export default class Point {
/**
* @param {MouseEvent} event
*/
static fromMouseEvent(event) {
return new Point(event.clientX, event.clientY);
}

/**
*
* @param {number} x
Expand All @@ -10,7 +21,31 @@ export default class Point {
}

/**
*
* @param {Point} point
*/
subtract(point) {
return new Point(this.x - point.x, this.y - point.y);
}

/**
* @param {Point} point
*/
add(point) {
return new Point(this.x - point.x, this.y - point.y);
}

/**
* @param {number} scalar
*/
multiply(scalar) {
return new Point(this.x * scalar, this.y * scalar);
}

get length() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}

/**
* @param {Point} point
*/
equals(point) {
Expand Down

0 comments on commit 8b00907

Please sign in to comment.