Skip to content

Commit

Permalink
Resolver: Animate camera, add sidebar (#55590)
Browse files Browse the repository at this point in the history
This PR adds a sidebar navigation. clicking the icons in the nav will focus the camera on the different nodes. There is an animation effect when the camera moves.
  • Loading branch information
Robert Austin authored Feb 14, 2020
1 parent c965a9e commit a790f61
Show file tree
Hide file tree
Showing 39 changed files with 2,226 additions and 615 deletions.
3 changes: 2 additions & 1 deletion x-pack/plugins/endpoint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"react-redux": "^7.1.0"
},
"devDependencies": {
"@types/react-redux": "^7.1.0"
"@types/react-redux": "^7.1.0",
"redux-devtools-extension": "^2.13.8"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Introduction

Resolver renders a map in a DOM element. Items on the map are placed in 2 dimensions using arbitrary units. Like other mapping software, the map can show things at different scales. The 'camera' determines what is shown on the map.

The camera is positioned. When the user clicks-and-drags the map, the camera's position is changed. This allows the user to pan around the map and see things that would otherwise be out of view, at a given scale.

The camera determines the scale. If the scale is smaller, the viewport of the map is larger and more is visible. This allows the user to zoom in an out. On screen controls and gestures (trackpad-pinch, or CTRL-mousewheel) change the scale.

# Concepts

## Scaling
The camera scale is controlled both by the user and programatically by Resolver. There is a maximum and minimum scale value (at the time of this writing they are 0.5 and 6.) This means that the map, and things on the map, will be rendered at between 0.5 and 6 times their instrinsic dimensions.

A range control is provided so that the user can change the scale. The user can also pinch-to-zoom on Mac OS X (or use ctrl-mousewheel otherwise) to change the scale. These interactions change the `scalingFactor`. This number is between 0 and 1. It represents how zoomed-in things should be. When the `scalingFactor` is 1, the scale will be the maximum scale value. When `scalingFactor` is 0, the scale will be the minimum scale value. Otherwise we interpolate between the minimum and maximum scale factor. The rate that the scale increases between the two is controlled by `scalingFactor**zoomCurveRate` The zoom curve rate is 4 at the time of this writing. This makes it so that the change in scale is more pronounced when the user is zoomed in.

```
renderScale = minimumScale * (1 - scalingFactor**curveRate) + maximumScale * scalingFactor**curveRate;
```

## Panning
When the user clicks and drags the map, the camera is 'moved' around. This allows the user to see different things on the map. The on-screen controls provide 4 directional buttons which nudge the camera, as well as a reset button. The reset button brings the camera back where it started (0, 0).

Resolver may programatically change the position of the camera in order to bring some interesting elements into view.

## Animation
The camera can animate changes to its position. Animations usually have a short, fixed duration, such as 1 second. If the camera is moving a great deal during the animation, then things could end up moving across the screen too quickly. In this case, looking at Resolver might be disorienting. In order to combat this, Resolver may temporarily decrease the scale. By decreasing the scale, objects look futher away. Far away objects appear to move slower.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import ReactDOM from 'react-dom';
import React from 'react';
import { AppRoot } from './view';
import { Provider } from 'react-redux';
import { Resolver } from './view';
import { storeFactory } from './store';
import { Embeddable } from '../../../../../../src/plugins/embeddable/public';

Expand All @@ -20,7 +21,12 @@ export class ResolverEmbeddable extends Embeddable {
}
this.lastRenderTarget = node;
const { store } = storeFactory();
ReactDOM.render(<AppRoot store={store} />, node);
ReactDOM.render(
<Provider store={store}>
<Resolver />
</Provider>,
node
);
}

public reload(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@
export function clamp(value: number, minimum: number, maximum: number) {
return Math.max(Math.min(value, maximum), minimum);
}

/**
* linearly interpolate between `a` and `b` at a ratio of `ratio`. If `ratio` is `0`, return `a`, if ratio is `1`, return `b`.
*/
export function lerp(a: number, b: number, ratio: number): number {
return a * (1 - ratio) + b * ratio;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,32 @@ export function inverseOrthographicProjection(
bottom: number,
left: number
): Matrix3 {
const m11 = (right - left) / 2;
const m13 = (right + left) / (right - left);
let m11: number;
let m13: number;
let m22: number;
let m23: number;

const m22 = (top - bottom) / 2;
const m23 = (top + bottom) / (top - bottom);
/**
* If `right - left` is 0, the width is 0, so scale everything to 0
*/
if (right - left === 0) {
m11 = 0;
m13 = 0;
} else {
m11 = (right - left) / 2;
m13 = (right + left) / (right - left);
}

/**
* If `top - bottom` is 0, the height is 0, so scale everything to 0
*/
if (top - bottom === 0) {
m22 = 0;
m23 = 0;
} else {
m22 = (top - bottom) / 2;
m23 = (top + bottom) / (top - bottom);
}

return [m11, 0, m13, 0, m22, m23, 0, 0, 0];
}
Expand All @@ -37,11 +58,32 @@ export function orthographicProjection(
bottom: number,
left: number
): Matrix3 {
const m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds
const m13 = -((right + left) / (right - left));
let m11: number;
let m13: number;
let m22: number;
let m23: number;

/**
* If `right - left` is 0, the width is 0, so scale everything to 0
*/
if (right - left === 0) {
m11 = 0;
m13 = 0;
} else {
m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds
m13 = -((right + left) / (right - left));
}

const m22 = 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds
const m23 = -((top + bottom) / (top - bottom));
/**
* If `top - bottom` is 0, the height is 0, so scale everything to 0
*/
if (top - bottom === 0) {
m22 = 0;
m23 = 0;
} else {
m22 = top - bottom === 0 ? 0 : 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds
m23 = top - bottom === 0 ? 0 : -((top + bottom) / (top - bottom));
}

return [m11, 0, m13, 0, m22, m23, 0, 0, 0];
}
Expand All @@ -68,6 +110,6 @@ export function translationTransformation([x, y]: Vector2): Matrix3 {
return [
1, 0, x,
0, 1, y,
0, 0, 1
0, 0, 0
]
}
37 changes: 37 additions & 0 deletions x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export function divide(a: Vector2, b: Vector2): Vector2 {
return [a[0] / b[0], a[1] / b[1]];
}

/**
* Return `[ a[0] * b[0], a[1] * b[1] ]`
*/
export function multiply(a: Vector2, b: Vector2): Vector2 {
return [a[0] * b[0], a[1] * b[1]];
}

/**
* Returns a vector which is the result of applying a 2D transformation matrix to the provided vector.
*/
Expand All @@ -50,3 +57,33 @@ export function angle(a: Vector2, b: Vector2) {
const deltaY = b[1] - a[1];
return Math.atan2(deltaY, deltaX);
}

/**
* Clamp `vector`'s components.
*/
export function clamp([x, y]: Vector2, [minX, minY]: Vector2, [maxX, maxY]: Vector2): Vector2 {
return [Math.max(minX, Math.min(maxX, x)), Math.max(minY, Math.min(maxY, y))];
}

/**
* Scale vector by number
*/
export function scale(a: Vector2, n: number): Vector2 {
return [a[0] * n, a[1] * n];
}

/**
* Linearly interpolate between `a` and `b`.
* `t` represents progress and:
* 0 <= `t` <= 1
*/
export function lerp(a: Vector2, b: Vector2, t: number): Vector2 {
return add(scale(a, 1 - t), scale(b, t));
}

/**
* The length of the vector
*/
export function length([x, y]: Vector2): number {
return Math.sqrt(x * x + y * y);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function mockProcessEvent(
machine_id: '',
...parts,
data_buffer: {
timestamp_utc: '2019-09-24 01:47:47Z',
event_subtype_full: 'creation_event',
event_type_full: 'process_event',
process_name: '',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ProcessEvent } from '../types';
import { CameraAction } from './camera';
import { DataAction } from './data';

/**
* When the user wants to bring a process node front-and-center on the map.
*/
interface UserBroughtProcessIntoView {
readonly type: 'userBroughtProcessIntoView';
readonly payload: {
/**
* Used to identify the process node that should be brought into view.
*/
readonly process: ProcessEvent;
/**
* The time (since epoch in milliseconds) when the action was dispatched.
*/
readonly time: number;
};
}

export type ResolverAction = CameraAction | DataAction | UserBroughtProcessIntoView;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Vector2, PanDirection } from '../../types';
import { Vector2 } from '../../types';

interface TimestampedPayload {
/**
* Time (since epoch in milliseconds) when this action was dispatched.
*/
readonly time: number;
}

interface UserSetZoomLevel {
readonly type: 'userSetZoomLevel';
Expand All @@ -24,11 +31,13 @@ interface UserClickedZoomIn {

interface UserZoomed {
readonly type: 'userZoomed';
/**
* A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`,
* pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels.
*/
readonly payload: number;
readonly payload: {
/**
* A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`,
* pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels.
*/
readonly zoomChange: number;
} & TimestampedPayload;
}

interface UserSetRasterSize {
Expand All @@ -40,7 +49,7 @@ interface UserSetRasterSize {
}

/**
* This is currently only used in tests. The 'back to center' button will use this action, and more tests around its behavior will need to be added.
* When the user warps the camera to an exact point instantly.
*/
interface UserSetPositionOfCamera {
readonly type: 'userSetPositionOfCamera';
Expand All @@ -52,33 +61,45 @@ interface UserSetPositionOfCamera {

interface UserStartedPanning {
readonly type: 'userStartedPanning';
/**
* A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen)
* relative to the Resolver component.
* Represents a starting position during panning for a pointing device.
*/
readonly payload: Vector2;

readonly payload: {
/**
* A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen)
* relative to the Resolver component.
* Represents a starting position during panning for a pointing device.
*/
readonly screenCoordinates: Vector2;
} & TimestampedPayload;
}

interface UserStoppedPanning {
readonly type: 'userStoppedPanning';

readonly payload: TimestampedPayload;
}

interface UserClickedPanControl {
readonly type: 'userClickedPanControl';
interface UserNudgedCamera {
readonly type: 'userNudgedCamera';
/**
* String that represents the direction in which Resolver can be panned
*/
readonly payload: PanDirection;
readonly payload: {
/**
* A cardinal direction to move the users perspective in.
*/
readonly direction: Vector2;
} & TimestampedPayload;
}

interface UserMovedPointer {
readonly type: 'userMovedPointer';
/**
* A vector in screen coordinates relative to the Resolver component.
* The payload should be contain clientX and clientY minus the client position of the Resolver component.
*/
readonly payload: Vector2;
readonly payload: {
/**
* A vector in screen coordinates relative to the Resolver component.
* The payload should be contain clientX and clientY minus the client position of the Resolver component.
*/
screenCoordinates: Vector2;
} & TimestampedPayload;
}

export type CameraAction =
Expand All @@ -91,4 +112,4 @@ export type CameraAction =
| UserMovedPointer
| UserClickedZoomOut
| UserClickedZoomIn
| UserClickedPanControl;
| UserNudgedCamera;
Loading

0 comments on commit a790f61

Please sign in to comment.