Skip to content

Commit

Permalink
Updated documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
anvaka committed Sep 21, 2017
1 parent 097873d commit 8844c2c
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 25 deletions.
76 changes: 58 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ngraph.path demo

[![demo](https://raw.githubusercontent.com/anvaka/ngraph.path/master/docs/seattle.gif)](https://anvaka.github.io/ngraph.path.demo/)

This repository is a demo for the [ngraph.path](https://github.com/anvaka/ngraph.path) library.
While its main purpose is to show the capabilities of the library, below you can find some
design decisions for the demo itself.
Expand Down Expand Up @@ -123,14 +125,13 @@ about what is going on inside:

Now that we have a graph, it's time to show it on the screen. Obviously,
we cannot use SVG to show millions of elements - that would be impossibly slow.
One way to go about it would be to generate tiles, and use something like [leaflet](http://leafletjs.com/) or [OpenSeadragon](https://openseadragon.github.io/)
One way to go about it would be to generate tiles, and use something like [Leaflet](http://leafletjs.com/) or [OpenSeadragon](https://openseadragon.github.io/)
to render a map.

I wanted to have more control over the code (as well as learn more about WebGL),
so I built a WebGL renderer from scratch. It employs a "scene graph" paradigm,
where scene is constructed from primitive elements, and rendering with transformations
recursively applied through the tree during rendering.

where scene is constructed from primitive elements. During frame rendering, the
scene graph is traversed, and nodes are given opportunity to refresh their presentation.

*NOTE: The renderer is [available here](https://github.com/anvaka/wgl), but it is
intentionally under-documented. I'm not planning to "release" it yet,
Expand All @@ -145,30 +146,65 @@ I realized that this makes mobile device very hot very soon, and the battery goe
`100%` to `0%` in a quick fashion.

This was especially painful during programming. I was working on this demo in my spare
time from coffee shops, with limited access to power.
time from coffee shops, with limited access to power. So I had to either think faster
or find a way to conserve energy :).

I still haven't figured out how to think faster, so I tried the latter approach.
Turns out solution was simple:

> Don't render scene on every single frame. In fact, render it only when explicitly asked,
> Don't render scene on every single frame. Render it only when explicitly asked,
> or when we know for sure that the scene was changed.
This may sound too obvious now, but it wasn't before. Most WebGL tutorials suggest a simple
loop:

``` js
function frame() {
requestAnimationFrame(frame); // schedule next frame;

renderScene(); // render current frame.
// nothing wrong with this, but this may drain battery quickly
}
```

With "conservative" approach, I had to move `requestAnimationFrame` outside from the `frame()` method:

``` js
let frameToken = 0;

function renderFrame() {
if (!frameToken) frameToken = requestAnimationFrame(frame);
}

function frame() {
frameToken = 0;
renderScene();
}
```

This approach allows anybody to schedule next frame in response to actions. For example,
when user drags scene and changes transformation matrix, we can call `renderFrame()` to update the scene.

The `frameToken` de-dupes multiple calls to `renderFrame()`.

Yes, conservative approach required a little bit more work, but at the end, battery life was amazing.

### Text and lines

WebGL is not the most intuitive framework. It is notoriously hard to deal with text and
"wide lines" (lines with width greater than 1px) in WebGL.
"wide lines" (i.e. lines with width greater than 1px) in WebGL.

![zoom-scale](https://raw.githubusercontent.com/anvaka/ngraph.path.demo/master/docs/zoom-scale.gif)

As I'm still learning WebGL, I realize that it would take me long time to build
a decent wide lines rendering or add text support.

On the other hand, I want wide lines and text only to render a path. A few DOM nodes
On the other hand, I want wide lines and text only to show a path. A few DOM nodes
should be enough...

Turns out, it was straightforward to add [a new element](https://github.com/anvaka/ngraph.path.demo/blob/master/src/SVGContainer.js)
to the scene graph, which applies transforms to SVG element. The SVG element is
given transparent background and `pointer-events: none;`, so it's completely invisible
given transparent background and `pointer-events: none;` so it's completely invisible
from interaction standpoint:

![svg overlay](https://raw.githubusercontent.com/anvaka/ngraph.path.demo/master/docs/svg-overlay.png)
Expand All @@ -177,10 +213,12 @@ from interaction standpoint:

I wanted to make pan and zoom interaction similar to what you would normally expect from a website like Google Maps.

I've already implemented a pan/zoom library for SVG: [anvaka/panzoom](https://github.com/anvaka/panzoom).
I've already implemented a pan/zoom library for SVG: [anvaka/panzoom](https://github.com/anvaka/panzoom).
With few changes to the code, I decoupled transform computation from transform application.

With few changes to the code, I decoupled function that applies transformation from behavior that calculates
transformation matrix.
So, panzoom listens to input events (`mousedown`, `touchstart`, etc.), performs smooth transition on
a transformation matrix, and forwards this matrix to a "controller". It is responsibility of the
controller to apply transforms.

This is not yet documented in the `panzoom` library, but this is all it takes to enable pan/zoom in WebGL:

Expand All @@ -190,22 +228,23 @@ This is not yet documented in the `panzoom` library, but this is all it takes to
## Hit testing

At this point we discussed how the data is loaded, how it is rendered, and how we can move around the graph.
But how do we know which point is being clicked? Where is the `start` and the `end` of the path?
But how do we know which point is being clicked? Where are the `start` and the `end` points of the path?

When we click on the scene, we could naively iterate over all points and find the nearest point to our
click. In fact, this is a decent solution if you have a thousand points or less. In our case, with several
hundred thousands points, that would be very slow.

I used a [QuadTree](https://en.wikipedia.org/wiki/Quadtree) to build index of points. After QuadTree is created,
you can query it in logarithmic time for the nearest neighbors.
you can query it in logarithmic time for the nearest neighbors. While `QuadTree` sounds scarry, it's not very much
different from a regular binary tree. It is easy to learn, easy to build and use.

In particular, I used [yaqt](https://github.com/anvaka/yaqt) library, because it had minimal memory overhead for
my data format. There are better alternatives that you might want to try as well (for example,
In particular, I used my own [yaqt](https://github.com/anvaka/yaqt) library, because it had minimal memory overhead for
my data layout. There are better alternatives that you might want to try as well (for example,
[d3-quadtree](https://github.com/d3/d3-quadtree)).

## The path finding

We have all pieces in place, we have the graph, we know how to render it and we now know what was clicked.
We have all pieces in place: we have the graph, we know how to render it, and we know what was clicked.
Now it's time to find the shortest path:

``` js
Expand All @@ -217,7 +256,8 @@ let path = pathFinder.find(fromId, toId);
let end = window.performance.now() - start;
```

This is exactly what happens in our [application model]( https://github.com/anvaka/ngraph.path.demo/blob/d8d5d3f6551a3b8eec5792e53f84577644c210b9/src/appModel.js#L110-L112).
I was contemplating about adding asynchronous path finding, but decided to put that work off, until it is really
necessary (let me know).

# Developing locally

Expand Down
Binary file added docs/preview_large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/seattle.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta property="og:title" content="Fast path finding library ngraph.path" />
<meta property="og:image" content="https://raw.githubusercontent.com/anvaka/ngraph.path.demo/master/docs/preview_large.png" />
<meta property="og:description" content="This is a demo of fast path finding algorithm for arbitrary graphs." />

<meta http-equiv='content-type' content='text/html; charset=utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
<meta http-equiv='X-UA-Compatible' content='IE=edge' >
<meta charset="utf-8">
<meta name="Description" content="This is a demo of fast path finding algorithm for arbitrary graphs.">
<meta name="keywords" content="a star, nba star, a*, path finding, shortest path, graph, map, visualization" />
<meta name="author" content="Andrei Kashcha">
<title>Fast path finding with ngraph</title>
<title>Fast path finding library ngraph.path</title>
<style>
* {
box-sizing: border-box;
Expand Down
30 changes: 24 additions & 6 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,13 @@
<option v-for='algorithm in pathFinder.algorithms' :value='algorithm.value'>{{algorithm.name}}</option>
</select>
</div>
<div class='row'>
<div class='label'>Graph:</div>
<select class='col' v-model='graphSettings.selected' @change='updateGraph'>
<option v-for='graph in graphSettings.graphs' :value='graph.value'>{{graph.name}}</option>
</select>
</div>
</div>
</div>
<div class='graph-name' v-if='!progress.visible'>
<select class='col' v-model='graphSettings.selected' @change='updateGraph'>
<option v-for='graph in graphSettings.graphs' :value='graph.value'>{{graph.name}}</option>
</select>
</div>
<div class='stats' v-if='loaded'>
<div>
Graph:
Expand Down Expand Up @@ -413,6 +412,19 @@ a.about-link {
padding: 7px 12px;
}
.graph-name {
position: absolute;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
background-color: hsla(215, 74%, 18%, 0.8);
padding: 0 10px;
select {
font-size: 24px;
cursor: pointer;
}
}
@media (max-width: 800px) {
.progress {
font-size: 18px;
Expand Down Expand Up @@ -459,6 +471,12 @@ a.about-link {
height: 300px;
}
}
.graph-name {
bottom: 40px;
select {
font-size: 16px;
}
}
}
svg.svg-overlay {
Expand Down
12 changes: 12 additions & 0 deletions src/components/About.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@
<script>
export default {
mounted() {
this.closeHandler = (e) => {
if (e.keyCode === 27) {
e.preventDefault();
this.close();
}
}
document.addEventListener('keyup', this.closeHandler);
},
beforeDestroy() {
document.removeEventListener('keyup', this.closeHandler);
},
methods: {
close() {
this.$emit('close');
Expand Down

0 comments on commit 8844c2c

Please sign in to comment.