mirror of
https://github.com/status-im/whispervis.git
synced 2025-02-12 21:16:27 +00:00
Initial commit
This commit is contained in:
commit
7d860ad225
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
whispervis
|
||||||
|
node_modules/
|
||||||
|
network.json
|
||||||
|
data.json
|
||||||
|
propagation.json
|
45
main.go
Normal file
45
main.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/divan/graph-experiments/graph"
|
||||||
|
"github.com/divan/graph-experiments/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
iterations := flag.Int("i", 600, "Graph layout iterations to run (0 = auto, buggy)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
data, err := graph.NewGraphFromJSON("network.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Printf("Loaded graph: %d nodes, %d links\n", len(data.Nodes()), len(data.Links()))
|
||||||
|
|
||||||
|
plog, err := LoadPropagationData("propagation.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Printf("Loaded propagation data: %d timestamps\n", len(plog.Timestamps))
|
||||||
|
|
||||||
|
log.Printf("Initializing layout...")
|
||||||
|
repelling := layout.NewGravityForce(-100.0, layout.BarneHutMethod)
|
||||||
|
springs := layout.NewSpringForce(0.01, 5.0, layout.ForEachLink)
|
||||||
|
drag := layout.NewDragForce(0.4, layout.ForEachNode)
|
||||||
|
layout3D := layout.New(data, repelling, springs, drag)
|
||||||
|
|
||||||
|
ws := NewWSServer(layout3D)
|
||||||
|
if *iterations == 0 {
|
||||||
|
ws.layout.Calculate()
|
||||||
|
} else {
|
||||||
|
ws.layout.CalculateN(*iterations)
|
||||||
|
}
|
||||||
|
ws.updateGraph(data)
|
||||||
|
ws.updatePropagationData(plog)
|
||||||
|
|
||||||
|
log.Printf("Starting web server...")
|
||||||
|
startWeb(ws)
|
||||||
|
select {}
|
||||||
|
}
|
30
propagation.go
Normal file
30
propagation.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PropagationLog represnts log of p2p message propagation
|
||||||
|
// with relative timestamps (starting from T0).
|
||||||
|
type PropagationLog struct {
|
||||||
|
Timestamps []int // timestamps in milliseconds starting from T0
|
||||||
|
Indices [][]int // indices of links for each step, len should be equal to len of Timestamps field
|
||||||
|
Nodes [][]int // indices of nodes involved
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadPropagationData(filename string) (*PropagationLog, error) {
|
||||||
|
fd, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
var plog PropagationLog
|
||||||
|
err = json.NewDecoder(fd).Decode(&plog)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &plog, nil
|
||||||
|
}
|
53
web.go
Normal file
53
web.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//go:generate browserify web/index.js web/js/ws.js -o web/bundle.js
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startWeb(ws *WSServer) {
|
||||||
|
port := ":20002"
|
||||||
|
go func() {
|
||||||
|
fs := http.FileServer(http.Dir("web"))
|
||||||
|
http.Handle("/", noCacheMiddleware(fs))
|
||||||
|
http.HandleFunc("/ws", ws.Handle)
|
||||||
|
log.Fatal(http.ListenAndServe(port, nil))
|
||||||
|
}()
|
||||||
|
url := "http://localhost" + port
|
||||||
|
fmt.Println("Please go to this url:", url)
|
||||||
|
/*
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
startBrowser(url)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// startBrowser tries to open the URL in a browser
|
||||||
|
// and reports whether it succeeds.
|
||||||
|
//
|
||||||
|
// Orig. code: golang.org/x/tools/cmd/cover/html.go
|
||||||
|
func startBrowser(url string) error {
|
||||||
|
// try to start the browser
|
||||||
|
var args []string
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
args = []string{"open"}
|
||||||
|
case "windows":
|
||||||
|
args = []string{"cmd", "/c", "start"}
|
||||||
|
default:
|
||||||
|
args = []string{"xdg-open"}
|
||||||
|
}
|
||||||
|
cmd := exec.Command(args[0], append(args[1:], url)...)
|
||||||
|
fmt.Println("If browser window didn't appear, please go to this url:", url)
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func noCacheMiddleware(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("Cache-Control", "max-age=0, no-cache")
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
5712
web/bundle.js
Normal file
5712
web/bundle.js
Normal file
File diff suppressed because one or more lines are too long
11
web/css/controls.css
Normal file
11
web/css/controls.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
body {
|
||||||
|
background: #c0c0c0 -webkit-gradient(linear, left top, left bottom, from(#fff), to(#c0c0c0)) no-repeat;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
height: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: "Helvetica Neue";
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
11
web/css/pure-min.css
vendored
Normal file
11
web/css/pure-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
19
web/data/data.js
Normal file
19
web/data/data.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
var graphData = {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "Foo",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Bar",
|
||||||
|
"group": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"source": "Foo",
|
||||||
|
"target": "Bar",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
1
web/data/data/labels.json
Normal file
1
web/data/data/labels.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
["Foo","Bar"]
|
BIN
web/data/data/links.bin
Normal file
BIN
web/data/data/links.bin
Normal file
Binary file not shown.
1
web/data/data/meta.json
Normal file
1
web/data/data/meta.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"date":1512329825919,"nodeCount":2,"linkCount":1,"nodeFile":"data/labels.json","linkFile":"data/links.bin","version":"1.0.1"}
|
BIN
web/data/data/positions.bin
Normal file
BIN
web/data/data/positions.bin
Normal file
Binary file not shown.
299
web/data/data_orig.json
Normal file
299
web/data/data_orig.json
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "\"Back()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"Element\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"Front()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"Init()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"Len()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"List\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"MoveAfter()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"MoveBefore()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"MoveToBack()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"MoveToFront()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"New()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"Next()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"Prev()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"PushBack()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"PushBackList()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"PushFront()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"PushFrontList()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"Remove\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"insert()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"insertValue()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"lazyInit()\"",
|
||||||
|
"group": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "\"remove()\"",
|
||||||
|
"group": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"source": "\"Element\"",
|
||||||
|
"target": "\"Element\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"Element\"",
|
||||||
|
"target": "\"Next()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"Element\"",
|
||||||
|
"target": "\"Prev()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"Back()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"Element\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"Front()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"Init()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"Len()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"MoveAfter()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"MoveBefore()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"MoveToBack()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"MoveToFront()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"PushBack()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"PushBackList()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"PushFront()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"PushFrontList()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"List\"",
|
||||||
|
"target": "\"Remove\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveAfter()\"",
|
||||||
|
"target": "\"insert()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveAfter()\"",
|
||||||
|
"target": "\"remove()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveBefore()\"",
|
||||||
|
"target": "\"insert()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveBefore()\"",
|
||||||
|
"target": "\"remove()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveToBack()\"",
|
||||||
|
"target": "\"insert()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveToBack()\"",
|
||||||
|
"target": "\"remove()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveToFront()\"",
|
||||||
|
"target": "\"insert()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"MoveToFront()\"",
|
||||||
|
"target": "\"remove()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"New()\"",
|
||||||
|
"target": "\"Init()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushBack()\"",
|
||||||
|
"target": "\"insertValue()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushBack()\"",
|
||||||
|
"target": "\"lazyInit()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushBackList()\"",
|
||||||
|
"target": "\"Front()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushBackList()\"",
|
||||||
|
"target": "\"Len()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushBackList()\"",
|
||||||
|
"target": "\"insertValue()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushBackList()\"",
|
||||||
|
"target": "\"lazyInit()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushFront()\"",
|
||||||
|
"target": "\"insertValue()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushFront()\"",
|
||||||
|
"target": "\"lazyInit()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushFrontList()\"",
|
||||||
|
"target": "\"Back()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushFrontList()\"",
|
||||||
|
"target": "\"Len()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushFrontList()\"",
|
||||||
|
"target": "\"insertValue()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"PushFrontList()\"",
|
||||||
|
"target": "\"lazyInit()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"Remove\"",
|
||||||
|
"target": "\"remove()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"insertValue()\"",
|
||||||
|
"target": "\"insert()\"",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "\"lazyInit()\"",
|
||||||
|
"target": "\"Init()\"",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
54
web/index.html
Normal file
54
web/index.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Status Whisper network graph</title>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; }
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="css/controls.css" type="text/css">
|
||||||
|
<link rel="stylesheet" href="css/pure-min.css" type="text/css">
|
||||||
|
<style>
|
||||||
|
.button-large {
|
||||||
|
font-size: 100%;
|
||||||
|
width: 100px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-success {
|
||||||
|
background: rgb(28, 184, 65);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
color: white;
|
||||||
|
background: rgb(66, 184, 221);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-error {
|
||||||
|
background: rgb(202, 60, 60);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1-5">
|
||||||
|
<h1>Status Whisper Network Graph</h1>
|
||||||
|
<p>This graph represents Status cluster and connected peers, as
|
||||||
|
reported by instrumented nodes and Status app with metrics option
|
||||||
|
enabled.</p>
|
||||||
|
<div>
|
||||||
|
<button class="pure-button pure-button-primary" id="replayButton">Replay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-4-5">
|
||||||
|
<canvas id="preview"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="node_modules/three/build/three.min.js"></script>
|
||||||
|
<script src="js/controls/TrackballControls.js"></script>
|
||||||
|
<script src="js/controls/FlyControls.js"></script>
|
||||||
|
|
||||||
|
<script src="bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
239
web/index.js
Normal file
239
web/index.js
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
var { colorStr2Hex, autoColorNodes } = require('./js/colors.js');
|
||||||
|
require('./js/keys.js');
|
||||||
|
var accessorFn = require('./js/shitty_hacks.js');
|
||||||
|
var { animatePropagation, restoreTimeout, updateRestoreTimeout, delayFactor, updateDelayFactor } = require('./js/animation.js');
|
||||||
|
var { NewEthereumGeometry } = require('./js/ethereum.js');
|
||||||
|
|
||||||
|
var Stats = require('stats-js');
|
||||||
|
const dat = require('dat.gui');
|
||||||
|
|
||||||
|
|
||||||
|
// WebGL
|
||||||
|
let canvas = document.getElementById("preview");
|
||||||
|
var renderer = new THREE.WebGLRenderer({ canvas: canvas });
|
||||||
|
|
||||||
|
var graphData, plog;
|
||||||
|
var positions = Array();
|
||||||
|
|
||||||
|
function setGraphData(data) {
|
||||||
|
graphData = data;
|
||||||
|
|
||||||
|
initGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPropagation(plogData) {
|
||||||
|
plog = plogData;
|
||||||
|
animatePropagation(nodesGroup, linksGroup, plogData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePositions(data) {
|
||||||
|
positions = data;
|
||||||
|
|
||||||
|
redrawGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { updatePositions, setGraphData, setPropagation };
|
||||||
|
|
||||||
|
// Setup scene
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x000011);
|
||||||
|
|
||||||
|
// Add lights
|
||||||
|
scene.add(new THREE.AmbientLight(0xbbbbbb));
|
||||||
|
scene.add(new THREE.DirectionalLight(0xffffff, 0.6));
|
||||||
|
|
||||||
|
var linksGroup = new THREE.Group();
|
||||||
|
scene.add(linksGroup);
|
||||||
|
var nodesGroup = new THREE.Group();
|
||||||
|
scene.add(nodesGroup);
|
||||||
|
|
||||||
|
// Setup camera
|
||||||
|
var camera = new THREE.PerspectiveCamera();
|
||||||
|
camera.far = 20000;
|
||||||
|
|
||||||
|
var tbControls = new THREE.TrackballControls(camera, renderer.domElement);
|
||||||
|
var flyControls = new THREE.FlyControls(camera, renderer.domElement);
|
||||||
|
|
||||||
|
var animate = function () {
|
||||||
|
// frame cycle
|
||||||
|
tbControls.update();
|
||||||
|
flyControls.update(1);
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
stats.update();
|
||||||
|
requestAnimationFrame( animate );
|
||||||
|
};
|
||||||
|
|
||||||
|
var width = window.innerWidth * 80 / 100 - 20;
|
||||||
|
var height = window.innerHeight - 20;
|
||||||
|
var nodeRelSize = 1;
|
||||||
|
var nodeResolution = 8;
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
var stats = new Stats();
|
||||||
|
document.body.appendChild( stats.domElement );
|
||||||
|
stats.domElement.style.position = 'absolute';
|
||||||
|
stats.domElement.style.right = '15px';
|
||||||
|
stats.domElement.style.bottom = '20px';
|
||||||
|
|
||||||
|
// Dat GUI
|
||||||
|
const gui = new dat.GUI();
|
||||||
|
var f1 = gui.addFolder('Animation');
|
||||||
|
var restoreCtl = f1.add({ restoreTimeout: restoreTimeout }, 'restoreTimeout').name('Restore timeout');
|
||||||
|
restoreCtl.onFinishChange(function(value) {
|
||||||
|
updateRestoreTimeout(value);
|
||||||
|
});
|
||||||
|
var factorCtl = f1.add({ delayFactor: delayFactor }, 'delayFactor').name('Delay factor');
|
||||||
|
factorCtl.onFinishChange(function(value) {
|
||||||
|
updateDelayFactor(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
var initGraph = function () {
|
||||||
|
resizeCanvas();
|
||||||
|
|
||||||
|
// parse links
|
||||||
|
graphData.links.forEach(link => {
|
||||||
|
link.source = link["source"];
|
||||||
|
link.target = link["target"];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add WebGL objects
|
||||||
|
// Clear the place
|
||||||
|
while (nodesGroup.children.length) {
|
||||||
|
nodesGroup.remove(nodesGroup.children[0])
|
||||||
|
}
|
||||||
|
while (linksGroup.children.length) {
|
||||||
|
linksGroup.remove(linksGroup.children[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render nodes
|
||||||
|
const nameAccessor = accessorFn("name");
|
||||||
|
const valAccessor = accessorFn("weight");
|
||||||
|
const colorAccessor = accessorFn("color");
|
||||||
|
let sphereGeometries = {}; // indexed by node value
|
||||||
|
let sphereMaterials = {}; // indexed by color
|
||||||
|
|
||||||
|
autoColorNodes(graphData.nodes);
|
||||||
|
graphData.nodes.forEach((node, idx) => {
|
||||||
|
let val = valAccessor(node) || 1;
|
||||||
|
if (!sphereGeometries.hasOwnProperty(val)) {
|
||||||
|
sphereGeometries[val] = NewEthereumGeometry(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = colorAccessor(node);
|
||||||
|
if (!sphereMaterials.hasOwnProperty(color)) {
|
||||||
|
sphereMaterials[color] = new THREE.MeshStandardMaterial({
|
||||||
|
color: colorStr2Hex(color || '#00ff00'),
|
||||||
|
transparent: false,
|
||||||
|
opacity: 0.75
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sphere = new THREE.Mesh(sphereGeometries[val], sphereMaterials[color]);
|
||||||
|
|
||||||
|
sphere.name = nameAccessor(node); // Add label
|
||||||
|
sphere.__data = node; // Attach node data
|
||||||
|
|
||||||
|
nodesGroup.add(node.__sphere = sphere);
|
||||||
|
if (positions[idx] !== undefined) {
|
||||||
|
sphere.position.set(positions[idx].x, positions[idx].y, positions[idx].z);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkColorAccessor = accessorFn("color");
|
||||||
|
let lineMaterials = {}; // indexed by color
|
||||||
|
console.log("Adding links", graphData.links.lengh);
|
||||||
|
graphData.links.forEach(link => {
|
||||||
|
const color = linkColorAccessor(link);
|
||||||
|
if (!lineMaterials.hasOwnProperty(color)) {
|
||||||
|
lineMaterials[color] = new THREE.LineBasicMaterial({
|
||||||
|
color: /*colorStr2Hex(color || '#f0f0f0')*/ '#f0f0f0',
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.4,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometry = new THREE.BufferGeometry();
|
||||||
|
geometry.addAttribute('position', new THREE.BufferAttribute(new Float32Array(2 * 3), 3));
|
||||||
|
const lineMaterial = lineMaterials[color];
|
||||||
|
const line = new THREE.Line(geometry, lineMaterial);
|
||||||
|
|
||||||
|
line.renderOrder = 10; // Prevent visual glitches of dark lines on top of spheres by rendering them last
|
||||||
|
|
||||||
|
linksGroup.add(link.__line = line);
|
||||||
|
});
|
||||||
|
|
||||||
|
// correct camera position
|
||||||
|
if (camera.position.x === 0 && camera.position.y === 0) {
|
||||||
|
// If camera still in default position (not user modified)
|
||||||
|
camera.lookAt(nodesGroup.position);
|
||||||
|
camera.position.z = Math.cbrt(graphData.nodes.length) * 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
if (width && height) {
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
camera.aspect = width/height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var redrawGraph = function () {
|
||||||
|
graphData.nodes.forEach((node, idx) => {
|
||||||
|
const sphere = node.__sphere;
|
||||||
|
if (!sphere) return;
|
||||||
|
|
||||||
|
sphere.position.x = positions[idx].x;
|
||||||
|
sphere.position.y = positions[idx].y || 0;
|
||||||
|
sphere.position.z = positions[idx].z || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
graphData.links.forEach(link => {
|
||||||
|
const line = link.__line;
|
||||||
|
if (!line) return;
|
||||||
|
|
||||||
|
linePos = line.geometry.attributes.position;
|
||||||
|
|
||||||
|
// TODO: move this index into map/cache or even into original graph data
|
||||||
|
let start, end;
|
||||||
|
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||||
|
if (graphData.nodes[i].id === link.source) {
|
||||||
|
start = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||||
|
if (graphData.nodes[i].id === link["target"]) {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
linePos.array[0] = positions[start].x;
|
||||||
|
linePos.array[1] = positions[start].y || 0;
|
||||||
|
linePos.array[2] = positions[start].z || 0;
|
||||||
|
linePos.array[3] = positions[end].x;
|
||||||
|
linePos.array[4] = positions[end].y || 0;
|
||||||
|
linePos.array[5] = positions[end].z || 0;
|
||||||
|
|
||||||
|
linePos.needsUpdate = true;
|
||||||
|
line.geometry.computeBoundingSphere();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// replay restarts propagation animation.
|
||||||
|
function replay() {
|
||||||
|
console.log(linksGroup, plog);
|
||||||
|
animatePropagation(nodesGroup, linksGroup, plog);
|
||||||
|
}
|
||||||
|
// js functions after browserify cannot be accessed from html,
|
||||||
|
// so instead of using onclick="replay()" we need to attach listener
|
||||||
|
// here.
|
||||||
|
// Did I already say that whole frontend ecosystem is a one giant
|
||||||
|
// museum of hacks for hacks on top of hacks?
|
||||||
|
var replayButton = document.getElementById('replayButton');
|
||||||
|
replayButton.addEventListener('click', replay);
|
||||||
|
|
||||||
|
animate();
|
79
web/js/animation.js
Normal file
79
web/js/animation.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
var gradient = require('d3-scale-chromatic').interpolateCool;
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mat = new THREE.LineBasicMaterial({
|
||||||
|
color: '#cc0000',
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.7
|
||||||
|
});
|
||||||
|
|
||||||
|
// Params
|
||||||
|
var restoreTimeout = 250; // ms
|
||||||
|
function updateRestoreTimeout(value) {
|
||||||
|
console.log("Updating restore timeout to ", value);
|
||||||
|
restoreTimeout = value;
|
||||||
|
}
|
||||||
|
var delayFactor = 3; // multiplication factor for blink timeout
|
||||||
|
function updateDelayFactor(value) {
|
||||||
|
delayFactor = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blinkLink(links, indices) {
|
||||||
|
indices.forEach(idx => {
|
||||||
|
if (links.children[idx].material === mat) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let oldMat = links.children[idx].material;
|
||||||
|
links.children[idx].material = mat;
|
||||||
|
|
||||||
|
console.log("Blinking with restore timeout", restoreTimeout);
|
||||||
|
setTimeout(function() {
|
||||||
|
links.children[idx].material = oldMat;
|
||||||
|
}, restoreTimeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeCounters = {};
|
||||||
|
var maxCounter = 0;
|
||||||
|
var nodeMaterials = {};
|
||||||
|
|
||||||
|
// blinkNodes updates nodes color increasing its temperature
|
||||||
|
// in a heatmap style
|
||||||
|
function blinkNodes(nodes, indices) {
|
||||||
|
indices.forEach(idx => {
|
||||||
|
nodeCounters[idx] = nodeCounters[idx] ? nodeCounters[idx]+1 : 1;
|
||||||
|
if (nodeCounters[idx] > maxCounter) {
|
||||||
|
maxCounter = nodeCounters[idx];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// TODO: FIXME: how to make simple map counter without this fucking bullshit?
|
||||||
|
Object.keys(nodeCounters).forEach(idx => {
|
||||||
|
let c = nodeCounters[idx];
|
||||||
|
let scale = c / maxCounter;
|
||||||
|
let color = gradient(scale);
|
||||||
|
if (nodeMaterials[color] === undefined) {
|
||||||
|
nodeMaterials[color] = new THREE.MeshStandardMaterial({color: new THREE.Color(color)});
|
||||||
|
}
|
||||||
|
nodes.children[idx].material = nodeMaterials[color];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function animatePropagation(nodes, links, plog) {
|
||||||
|
maxCounter = 0;
|
||||||
|
nodeCounters = {};
|
||||||
|
plog.Timestamps.forEach((ts, idx) => {
|
||||||
|
setTimeout(function() {
|
||||||
|
blinkLink(links, plog.Indices[idx]);
|
||||||
|
}, ts*delayFactor);
|
||||||
|
setTimeout(function() {
|
||||||
|
blinkNodes(nodes, plog.Nodes[idx]);
|
||||||
|
}, ts*delayFactor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { animatePropagation,
|
||||||
|
restoreTimeout, updateRestoreTimeout,
|
||||||
|
delayFactor, updateDelayFactor }
|
20
web/js/colors.js
Normal file
20
web/js/colors.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
var schemePaired = require('d3-scale-chromatic').schemePaired;
|
||||||
|
var tinyColor = require('tinycolor2');
|
||||||
|
|
||||||
|
const colorStr2Hex = str => isNaN(str) ? parseInt(tinyColor(str).toHex(), 16) : str;
|
||||||
|
|
||||||
|
function autoColorNodes(nodes) {
|
||||||
|
const colors = schemePaired; // Paired color set from color brewer
|
||||||
|
|
||||||
|
const uncoloredNodes = nodes.filter(node => !node.color);
|
||||||
|
const nodeGroups = {};
|
||||||
|
|
||||||
|
uncoloredNodes.forEach(node => { nodeGroups[node["group"]] = null });
|
||||||
|
Object.keys(nodeGroups).forEach((group, idx) => { nodeGroups[group] = idx });
|
||||||
|
|
||||||
|
uncoloredNodes.forEach(node => {
|
||||||
|
node.color = colorStr2Hex(colors[nodeGroups[node["group"]] % colors.length]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { colorStr2Hex, autoColorNodes };
|
292
web/js/controls/FlyControls.js
Normal file
292
web/js/controls/FlyControls.js
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* @author James Baicoianu / http://www.baicoianu.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
THREE.FlyControls = function ( object, domElement ) {
|
||||||
|
|
||||||
|
this.object = object;
|
||||||
|
|
||||||
|
this.domElement = ( domElement !== undefined ) ? domElement : document;
|
||||||
|
if ( domElement ) this.domElement.setAttribute( 'tabindex', - 1 );
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
this.movementSpeed = 1.0;
|
||||||
|
this.rollSpeed = 0.005;
|
||||||
|
|
||||||
|
this.dragToLook = false;
|
||||||
|
this.autoForward = false;
|
||||||
|
|
||||||
|
// disable default target object behavior
|
||||||
|
|
||||||
|
// internals
|
||||||
|
|
||||||
|
this.tmpQuaternion = new THREE.Quaternion();
|
||||||
|
|
||||||
|
this.mouseStatus = 0;
|
||||||
|
|
||||||
|
this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 };
|
||||||
|
this.moveVector = new THREE.Vector3( 0, 0, 0 );
|
||||||
|
this.rotationVector = new THREE.Vector3( 0, 0, 0 );
|
||||||
|
|
||||||
|
this.handleEvent = function ( event ) {
|
||||||
|
|
||||||
|
if ( typeof this[ event.type ] == 'function' ) {
|
||||||
|
|
||||||
|
this[ event.type ]( event );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.keydown = function( event ) {
|
||||||
|
|
||||||
|
if ( event.altKey ) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//event.preventDefault();
|
||||||
|
|
||||||
|
switch ( event.keyCode ) {
|
||||||
|
|
||||||
|
case 16: /* shift */ this.movementSpeedMultiplier = .1; break;
|
||||||
|
|
||||||
|
case 87: /*W*/ this.moveState.forward = 1; break;
|
||||||
|
case 83: /*S*/ this.moveState.back = 1; break;
|
||||||
|
|
||||||
|
case 65: /*A*/ this.moveState.left = 1; break;
|
||||||
|
case 68: /*D*/ this.moveState.right = 1; break;
|
||||||
|
|
||||||
|
case 82: /*R*/ this.moveState.up = 1; break;
|
||||||
|
case 70: /*F*/ this.moveState.down = 1; break;
|
||||||
|
|
||||||
|
case 38: /*up*/ this.moveState.pitchUp = 1; break;
|
||||||
|
case 40: /*down*/ this.moveState.pitchDown = 1; break;
|
||||||
|
|
||||||
|
case 37: /*left*/ this.moveState.yawLeft = 1; break;
|
||||||
|
case 39: /*right*/ this.moveState.yawRight = 1; break;
|
||||||
|
|
||||||
|
case 81: /*Q*/ this.moveState.rollLeft = 1; break;
|
||||||
|
case 69: /*E*/ this.moveState.rollRight = 1; break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMovementVector();
|
||||||
|
this.updateRotationVector();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.keyup = function( event ) {
|
||||||
|
|
||||||
|
switch ( event.keyCode ) {
|
||||||
|
|
||||||
|
case 16: /* shift */ this.movementSpeedMultiplier = 1; break;
|
||||||
|
|
||||||
|
case 87: /*W*/ this.moveState.forward = 0; break;
|
||||||
|
case 83: /*S*/ this.moveState.back = 0; break;
|
||||||
|
|
||||||
|
case 65: /*A*/ this.moveState.left = 0; break;
|
||||||
|
case 68: /*D*/ this.moveState.right = 0; break;
|
||||||
|
|
||||||
|
case 82: /*R*/ this.moveState.up = 0; break;
|
||||||
|
case 70: /*F*/ this.moveState.down = 0; break;
|
||||||
|
|
||||||
|
case 38: /*up*/ this.moveState.pitchUp = 0; break;
|
||||||
|
case 40: /*down*/ this.moveState.pitchDown = 0; break;
|
||||||
|
|
||||||
|
case 37: /*left*/ this.moveState.yawLeft = 0; break;
|
||||||
|
case 39: /*right*/ this.moveState.yawRight = 0; break;
|
||||||
|
|
||||||
|
case 81: /*Q*/ this.moveState.rollLeft = 0; break;
|
||||||
|
case 69: /*E*/ this.moveState.rollRight = 0; break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMovementVector();
|
||||||
|
this.updateRotationVector();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mousedown = function( event ) {
|
||||||
|
|
||||||
|
if ( this.domElement !== document ) {
|
||||||
|
|
||||||
|
this.domElement.focus();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if ( this.dragToLook ) {
|
||||||
|
|
||||||
|
this.mouseStatus ++;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
switch ( event.button ) {
|
||||||
|
|
||||||
|
case 0: this.moveState.forward = 1; break;
|
||||||
|
case 2: this.moveState.back = 1; break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMovementVector();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mousemove = function( event ) {
|
||||||
|
|
||||||
|
// (divan: disabled)
|
||||||
|
if (false && (! this.dragToLook || this.mouseStatus > 0)) {
|
||||||
|
var container = this.getContainerDimensions();
|
||||||
|
var halfWidth = container.size[ 0 ] / 2;
|
||||||
|
var halfHeight = container.size[ 1 ] / 2;
|
||||||
|
|
||||||
|
this.moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth;
|
||||||
|
this.moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight;
|
||||||
|
|
||||||
|
this.updateRotationVector();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mouseup = function( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if ( this.dragToLook ) {
|
||||||
|
|
||||||
|
this.mouseStatus --;
|
||||||
|
|
||||||
|
this.moveState.yawLeft = this.moveState.pitchDown = 0;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
switch ( event.button ) {
|
||||||
|
|
||||||
|
case 0: this.moveState.forward = 0; break;
|
||||||
|
case 2: this.moveState.back = 0; break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMovementVector();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateRotationVector();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update = function( delta ) {
|
||||||
|
|
||||||
|
var moveMult = delta * this.movementSpeed;
|
||||||
|
var rotMult = delta * this.rollSpeed;
|
||||||
|
|
||||||
|
this.object.translateX( this.moveVector.x * moveMult );
|
||||||
|
this.object.translateY( this.moveVector.y * moveMult );
|
||||||
|
this.object.translateZ( this.moveVector.z * moveMult );
|
||||||
|
|
||||||
|
this.tmpQuaternion.set( this.rotationVector.x * rotMult, this.rotationVector.y * rotMult, this.rotationVector.z * rotMult, 1 ).normalize();
|
||||||
|
this.object.quaternion.multiply( this.tmpQuaternion );
|
||||||
|
|
||||||
|
// expose the rotation vector for convenience
|
||||||
|
this.object.rotation.setFromQuaternion( this.object.quaternion, this.object.rotation.order );
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateMovementVector = function() {
|
||||||
|
|
||||||
|
var forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0;
|
||||||
|
|
||||||
|
this.moveVector.x = ( - this.moveState.left + this.moveState.right );
|
||||||
|
this.moveVector.y = ( - this.moveState.down + this.moveState.up );
|
||||||
|
this.moveVector.z = ( - forward + this.moveState.back );
|
||||||
|
|
||||||
|
//console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateRotationVector = function() {
|
||||||
|
|
||||||
|
this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp );
|
||||||
|
this.rotationVector.y = ( - this.moveState.yawRight + this.moveState.yawLeft );
|
||||||
|
this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft );
|
||||||
|
|
||||||
|
//console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getContainerDimensions = function() {
|
||||||
|
|
||||||
|
if ( this.domElement != document ) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
size : [ this.domElement.offsetWidth, this.domElement.offsetHeight ],
|
||||||
|
offset : [ this.domElement.offsetLeft, this.domElement.offsetTop ]
|
||||||
|
};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return {
|
||||||
|
size : [ window.innerWidth, window.innerHeight ],
|
||||||
|
offset : [ 0, 0 ]
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
function bind( scope, fn ) {
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
|
||||||
|
fn.apply( scope, arguments );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function contextmenu( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispose = function() {
|
||||||
|
|
||||||
|
//this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
|
||||||
|
//this.domElement.removeEventListener( 'mousedown', _mousedown, false );
|
||||||
|
//this.domElement.removeEventListener( 'mousemove', _mousemove, false );
|
||||||
|
//this.domElement.removeEventListener( 'mouseup', _mouseup, false );
|
||||||
|
|
||||||
|
window.removeEventListener( 'keydown', _keydown, false );
|
||||||
|
window.removeEventListener( 'keyup', _keyup, false );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
//var _mousemove = bind( this, this.mousemove );
|
||||||
|
var _mousedown = bind( this, this.mousedown );
|
||||||
|
var _mouseup = bind( this, this.mouseup );
|
||||||
|
var _keydown = bind( this, this.keydown );
|
||||||
|
var _keyup = bind( this, this.keyup );
|
||||||
|
|
||||||
|
this.domElement.addEventListener( 'contextmenu', contextmenu, false );
|
||||||
|
|
||||||
|
//this.domElement.addEventListener( 'mousemove', _mousemove, false );
|
||||||
|
//this.domElement.addEventListener( 'mousedown', _mousedown, false );
|
||||||
|
//this.domElement.addEventListener( 'mouseup', _mouseup, false );
|
||||||
|
|
||||||
|
window.addEventListener( 'keydown', _keydown, false );
|
||||||
|
window.addEventListener( 'keyup', _keyup, false );
|
||||||
|
|
||||||
|
this.updateMovementVector();
|
||||||
|
this.updateRotationVector();
|
||||||
|
|
||||||
|
};
|
625
web/js/controls/TrackballControls.js
Normal file
625
web/js/controls/TrackballControls.js
Normal file
@ -0,0 +1,625 @@
|
|||||||
|
/**
|
||||||
|
* @author Eberhard Graether / http://egraether.com/
|
||||||
|
* @author Mark Lundin / http://mark-lundin.com
|
||||||
|
* @author Simone Manini / http://daron1337.github.io
|
||||||
|
* @author Luca Antiga / http://lantiga.github.io
|
||||||
|
*/
|
||||||
|
|
||||||
|
THREE.TrackballControls = function ( object, domElement ) {
|
||||||
|
|
||||||
|
var _this = this;
|
||||||
|
var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
|
||||||
|
|
||||||
|
this.object = object;
|
||||||
|
this.domElement = ( domElement !== undefined ) ? domElement : document;
|
||||||
|
|
||||||
|
// API
|
||||||
|
|
||||||
|
this.enabled = true;
|
||||||
|
|
||||||
|
this.screen = { left: 0, top: 0, width: 0, height: 0 };
|
||||||
|
|
||||||
|
this.rotateSpeed = 1.0;
|
||||||
|
this.zoomSpeed = 1.2;
|
||||||
|
this.panSpeed = 0.3;
|
||||||
|
|
||||||
|
this.noRotate = false;
|
||||||
|
this.noZoom = false;
|
||||||
|
this.noPan = false;
|
||||||
|
|
||||||
|
this.staticMoving = false;
|
||||||
|
this.dynamicDampingFactor = 0.2;
|
||||||
|
|
||||||
|
this.minDistance = 0;
|
||||||
|
this.maxDistance = Infinity;
|
||||||
|
|
||||||
|
this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];
|
||||||
|
|
||||||
|
// internals
|
||||||
|
|
||||||
|
this.target = new THREE.Vector3();
|
||||||
|
|
||||||
|
var EPS = 0.000001;
|
||||||
|
|
||||||
|
var lastPosition = new THREE.Vector3();
|
||||||
|
|
||||||
|
var _state = STATE.NONE,
|
||||||
|
_prevState = STATE.NONE,
|
||||||
|
|
||||||
|
_eye = new THREE.Vector3(),
|
||||||
|
|
||||||
|
_movePrev = new THREE.Vector2(),
|
||||||
|
_moveCurr = new THREE.Vector2(),
|
||||||
|
|
||||||
|
_lastAxis = new THREE.Vector3(),
|
||||||
|
_lastAngle = 0,
|
||||||
|
|
||||||
|
_zoomStart = new THREE.Vector2(),
|
||||||
|
_zoomEnd = new THREE.Vector2(),
|
||||||
|
|
||||||
|
_touchZoomDistanceStart = 0,
|
||||||
|
_touchZoomDistanceEnd = 0,
|
||||||
|
|
||||||
|
_panStart = new THREE.Vector2(),
|
||||||
|
_panEnd = new THREE.Vector2();
|
||||||
|
|
||||||
|
// for reset
|
||||||
|
|
||||||
|
this.target0 = this.target.clone();
|
||||||
|
this.position0 = this.object.position.clone();
|
||||||
|
this.up0 = this.object.up.clone();
|
||||||
|
|
||||||
|
// events
|
||||||
|
|
||||||
|
var changeEvent = { type: 'change' };
|
||||||
|
var startEvent = { type: 'start' };
|
||||||
|
var endEvent = { type: 'end' };
|
||||||
|
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
this.handleResize = function () {
|
||||||
|
|
||||||
|
if ( this.domElement === document ) {
|
||||||
|
|
||||||
|
this.screen.left = 0;
|
||||||
|
this.screen.top = 0;
|
||||||
|
this.screen.width = window.innerWidth;
|
||||||
|
this.screen.height = window.innerHeight;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
var box = this.domElement.getBoundingClientRect();
|
||||||
|
// adjustments come from similar code in the jquery offset() function
|
||||||
|
var d = this.domElement.ownerDocument.documentElement;
|
||||||
|
this.screen.left = box.left + window.pageXOffset - d.clientLeft;
|
||||||
|
this.screen.top = box.top + window.pageYOffset - d.clientTop;
|
||||||
|
this.screen.width = box.width;
|
||||||
|
this.screen.height = box.height;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleEvent = function ( event ) {
|
||||||
|
|
||||||
|
if ( typeof this[ event.type ] == 'function' ) {
|
||||||
|
|
||||||
|
this[ event.type ]( event );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
var getMouseOnScreen = ( function () {
|
||||||
|
|
||||||
|
var vector = new THREE.Vector2();
|
||||||
|
|
||||||
|
return function getMouseOnScreen( pageX, pageY ) {
|
||||||
|
|
||||||
|
vector.set(
|
||||||
|
( pageX - _this.screen.left ) / _this.screen.width,
|
||||||
|
( pageY - _this.screen.top ) / _this.screen.height
|
||||||
|
);
|
||||||
|
|
||||||
|
return vector;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}() );
|
||||||
|
|
||||||
|
var getMouseOnCircle = ( function () {
|
||||||
|
|
||||||
|
var vector = new THREE.Vector2();
|
||||||
|
|
||||||
|
return function getMouseOnCircle( pageX, pageY ) {
|
||||||
|
|
||||||
|
vector.set(
|
||||||
|
( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ),
|
||||||
|
( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional
|
||||||
|
);
|
||||||
|
|
||||||
|
return vector;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}() );
|
||||||
|
|
||||||
|
this.rotateCamera = ( function() {
|
||||||
|
|
||||||
|
var axis = new THREE.Vector3(),
|
||||||
|
quaternion = new THREE.Quaternion(),
|
||||||
|
eyeDirection = new THREE.Vector3(),
|
||||||
|
objectUpDirection = new THREE.Vector3(),
|
||||||
|
objectSidewaysDirection = new THREE.Vector3(),
|
||||||
|
moveDirection = new THREE.Vector3(),
|
||||||
|
angle;
|
||||||
|
|
||||||
|
return function rotateCamera() {
|
||||||
|
|
||||||
|
moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
|
||||||
|
angle = moveDirection.length();
|
||||||
|
|
||||||
|
if ( angle ) {
|
||||||
|
|
||||||
|
_eye.copy( _this.object.position ).sub( _this.target );
|
||||||
|
|
||||||
|
eyeDirection.copy( _eye ).normalize();
|
||||||
|
objectUpDirection.copy( _this.object.up ).normalize();
|
||||||
|
objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
|
||||||
|
|
||||||
|
objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
|
||||||
|
objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
|
||||||
|
|
||||||
|
moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
|
||||||
|
|
||||||
|
axis.crossVectors( moveDirection, _eye ).normalize();
|
||||||
|
|
||||||
|
angle *= _this.rotateSpeed;
|
||||||
|
quaternion.setFromAxisAngle( axis, angle );
|
||||||
|
|
||||||
|
_eye.applyQuaternion( quaternion );
|
||||||
|
_this.object.up.applyQuaternion( quaternion );
|
||||||
|
|
||||||
|
_lastAxis.copy( axis );
|
||||||
|
_lastAngle = angle;
|
||||||
|
|
||||||
|
} else if ( ! _this.staticMoving && _lastAngle ) {
|
||||||
|
|
||||||
|
_lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor );
|
||||||
|
_eye.copy( _this.object.position ).sub( _this.target );
|
||||||
|
quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
|
||||||
|
_eye.applyQuaternion( quaternion );
|
||||||
|
_this.object.up.applyQuaternion( quaternion );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_movePrev.copy( _moveCurr );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}() );
|
||||||
|
|
||||||
|
|
||||||
|
this.zoomCamera = function () {
|
||||||
|
|
||||||
|
var factor;
|
||||||
|
|
||||||
|
if ( _state === STATE.TOUCH_ZOOM_PAN ) {
|
||||||
|
|
||||||
|
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
|
||||||
|
_touchZoomDistanceStart = _touchZoomDistanceEnd;
|
||||||
|
_eye.multiplyScalar( factor );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;
|
||||||
|
|
||||||
|
if ( factor !== 1.0 && factor > 0.0 ) {
|
||||||
|
|
||||||
|
_eye.multiplyScalar( factor );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( _this.staticMoving ) {
|
||||||
|
|
||||||
|
_zoomStart.copy( _zoomEnd );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.panCamera = ( function() {
|
||||||
|
|
||||||
|
var mouseChange = new THREE.Vector2(),
|
||||||
|
objectUp = new THREE.Vector3(),
|
||||||
|
pan = new THREE.Vector3();
|
||||||
|
|
||||||
|
return function panCamera() {
|
||||||
|
|
||||||
|
mouseChange.copy( _panEnd ).sub( _panStart );
|
||||||
|
|
||||||
|
if ( mouseChange.lengthSq() ) {
|
||||||
|
|
||||||
|
mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );
|
||||||
|
|
||||||
|
pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
|
||||||
|
pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );
|
||||||
|
|
||||||
|
_this.object.position.add( pan );
|
||||||
|
_this.target.add( pan );
|
||||||
|
|
||||||
|
if ( _this.staticMoving ) {
|
||||||
|
|
||||||
|
_panStart.copy( _panEnd );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}() );
|
||||||
|
|
||||||
|
this.checkDistances = function () {
|
||||||
|
|
||||||
|
if ( ! _this.noZoom || ! _this.noPan ) {
|
||||||
|
|
||||||
|
if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) {
|
||||||
|
|
||||||
|
_this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) );
|
||||||
|
_zoomStart.copy( _zoomEnd );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {
|
||||||
|
|
||||||
|
_this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );
|
||||||
|
_zoomStart.copy( _zoomEnd );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update = function () {
|
||||||
|
|
||||||
|
_eye.subVectors( _this.object.position, _this.target );
|
||||||
|
|
||||||
|
if ( ! _this.noRotate ) {
|
||||||
|
|
||||||
|
_this.rotateCamera();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! _this.noZoom ) {
|
||||||
|
|
||||||
|
_this.zoomCamera();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! _this.noPan ) {
|
||||||
|
|
||||||
|
_this.panCamera();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_this.object.position.addVectors( _this.target, _eye );
|
||||||
|
|
||||||
|
_this.checkDistances();
|
||||||
|
|
||||||
|
_this.object.lookAt( _this.target );
|
||||||
|
|
||||||
|
if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) {
|
||||||
|
|
||||||
|
_this.dispatchEvent( changeEvent );
|
||||||
|
|
||||||
|
lastPosition.copy( _this.object.position );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.reset = function () {
|
||||||
|
|
||||||
|
_state = STATE.NONE;
|
||||||
|
_prevState = STATE.NONE;
|
||||||
|
|
||||||
|
_this.target.copy( _this.target0 );
|
||||||
|
_this.object.position.copy( _this.position0 );
|
||||||
|
_this.object.up.copy( _this.up0 );
|
||||||
|
|
||||||
|
_eye.subVectors( _this.object.position, _this.target );
|
||||||
|
|
||||||
|
_this.object.lookAt( _this.target );
|
||||||
|
|
||||||
|
_this.dispatchEvent( changeEvent );
|
||||||
|
|
||||||
|
lastPosition.copy( _this.object.position );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// listeners
|
||||||
|
|
||||||
|
function keydown( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
window.removeEventListener( 'keydown', keydown );
|
||||||
|
|
||||||
|
_prevState = _state;
|
||||||
|
|
||||||
|
if ( _state !== STATE.NONE ) {
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) {
|
||||||
|
|
||||||
|
_state = STATE.ROTATE;
|
||||||
|
|
||||||
|
} else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) {
|
||||||
|
|
||||||
|
_state = STATE.ZOOM;
|
||||||
|
|
||||||
|
} else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) {
|
||||||
|
|
||||||
|
_state = STATE.PAN;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyup( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
_state = _prevState;
|
||||||
|
|
||||||
|
window.addEventListener( 'keydown', keydown, false );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function mousedown( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if ( _state === STATE.NONE ) {
|
||||||
|
|
||||||
|
_state = event.button;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( _state === STATE.ROTATE && ! _this.noRotate ) {
|
||||||
|
|
||||||
|
_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
|
||||||
|
_movePrev.copy( _moveCurr );
|
||||||
|
|
||||||
|
} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
|
||||||
|
|
||||||
|
_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||||
|
_zoomEnd.copy( _zoomStart );
|
||||||
|
|
||||||
|
} else if ( _state === STATE.PAN && ! _this.noPan ) {
|
||||||
|
|
||||||
|
_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||||
|
_panEnd.copy( _panStart );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener( 'mousemove', mousemove, false );
|
||||||
|
document.addEventListener( 'mouseup', mouseup, false );
|
||||||
|
|
||||||
|
_this.dispatchEvent( startEvent );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function mousemove( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if ( _state === STATE.ROTATE && ! _this.noRotate ) {
|
||||||
|
|
||||||
|
_movePrev.copy( _moveCurr );
|
||||||
|
_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
|
||||||
|
|
||||||
|
} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
|
||||||
|
|
||||||
|
_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||||
|
|
||||||
|
} else if ( _state === STATE.PAN && ! _this.noPan ) {
|
||||||
|
|
||||||
|
_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseup( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
_state = STATE.NONE;
|
||||||
|
|
||||||
|
document.removeEventListener( 'mousemove', mousemove );
|
||||||
|
document.removeEventListener( 'mouseup', mouseup );
|
||||||
|
_this.dispatchEvent( endEvent );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function mousewheel( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
switch ( event.deltaMode ) {
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// Zoom in pages
|
||||||
|
_zoomStart.y -= event.deltaY * 0.025;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// Zoom in lines
|
||||||
|
_zoomStart.y -= event.deltaY * 0.01;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// undefined, 0, assume pixels
|
||||||
|
_zoomStart.y -= event.deltaY * 0.00025;
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_this.dispatchEvent( startEvent );
|
||||||
|
_this.dispatchEvent( endEvent );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchstart( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
switch ( event.touches.length ) {
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
_state = STATE.TOUCH_ROTATE;
|
||||||
|
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
|
||||||
|
_movePrev.copy( _moveCurr );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 2 or more
|
||||||
|
_state = STATE.TOUCH_ZOOM_PAN;
|
||||||
|
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||||
|
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||||
|
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
|
||||||
|
|
||||||
|
var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
|
||||||
|
var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
|
||||||
|
_panStart.copy( getMouseOnScreen( x, y ) );
|
||||||
|
_panEnd.copy( _panStart );
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_this.dispatchEvent( startEvent );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchmove( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
switch ( event.touches.length ) {
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
_movePrev.copy( _moveCurr );
|
||||||
|
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 2 or more
|
||||||
|
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||||
|
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||||
|
_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
|
||||||
|
|
||||||
|
var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
|
||||||
|
var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
|
||||||
|
_panEnd.copy( getMouseOnScreen( x, y ) );
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchend( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
switch ( event.touches.length ) {
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
_state = STATE.NONE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
_state = STATE.TOUCH_ROTATE;
|
||||||
|
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
|
||||||
|
_movePrev.copy( _moveCurr );
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_this.dispatchEvent( endEvent );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function contextmenu( event ) {
|
||||||
|
|
||||||
|
if ( _this.enabled === false ) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispose = function() {
|
||||||
|
|
||||||
|
this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
|
||||||
|
this.domElement.removeEventListener( 'mousedown', mousedown, false );
|
||||||
|
this.domElement.removeEventListener( 'wheel', mousewheel, false );
|
||||||
|
|
||||||
|
this.domElement.removeEventListener( 'touchstart', touchstart, false );
|
||||||
|
this.domElement.removeEventListener( 'touchend', touchend, false );
|
||||||
|
this.domElement.removeEventListener( 'touchmove', touchmove, false );
|
||||||
|
|
||||||
|
document.removeEventListener( 'mousemove', mousemove, false );
|
||||||
|
document.removeEventListener( 'mouseup', mouseup, false );
|
||||||
|
|
||||||
|
window.removeEventListener( 'keydown', keydown, false );
|
||||||
|
window.removeEventListener( 'keyup', keyup, false );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
this.domElement.addEventListener( 'contextmenu', contextmenu, false );
|
||||||
|
this.domElement.addEventListener( 'mousedown', mousedown, false );
|
||||||
|
this.domElement.addEventListener( 'wheel', mousewheel, false );
|
||||||
|
|
||||||
|
this.domElement.addEventListener( 'touchstart', touchstart, false );
|
||||||
|
this.domElement.addEventListener( 'touchend', touchend, false );
|
||||||
|
this.domElement.addEventListener( 'touchmove', touchmove, false );
|
||||||
|
|
||||||
|
window.addEventListener( 'keydown', keydown, false );
|
||||||
|
window.addEventListener( 'keyup', keyup, false );
|
||||||
|
|
||||||
|
this.handleResize();
|
||||||
|
|
||||||
|
// force an update at start
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
|
||||||
|
THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;
|
26
web/js/ethereum.js
Normal file
26
web/js/ethereum.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
function NewEthereumGeometry(scale) {
|
||||||
|
let geom = new THREE.Geometry();
|
||||||
|
geom.vertices.push(
|
||||||
|
new THREE.Vector3( scale*1, 0, 0 ),
|
||||||
|
new THREE.Vector3( -scale*1, 0, 0 ),
|
||||||
|
new THREE.Vector3( 0, scale*1.5, 0 ),
|
||||||
|
new THREE.Vector3( 0, scale*-1.5, 0 ),
|
||||||
|
new THREE.Vector3( 0, 0, scale*1 ),
|
||||||
|
new THREE.Vector3( 0, 0, -scale*1 )
|
||||||
|
);
|
||||||
|
geom.faces.push(
|
||||||
|
new THREE.Face3( 0, 2, 4 ),
|
||||||
|
new THREE.Face3( 0, 4, 3 ),
|
||||||
|
new THREE.Face3( 0, 3, 5 ),
|
||||||
|
new THREE.Face3( 0, 5, 2 ),
|
||||||
|
new THREE.Face3( 1, 2, 5 ),
|
||||||
|
new THREE.Face3( 1, 5, 3 ),
|
||||||
|
new THREE.Face3( 1, 3, 4 ),
|
||||||
|
new THREE.Face3( 1, 4, 2 )
|
||||||
|
);
|
||||||
|
geom.computeBoundingSphere();
|
||||||
|
geom.computeFaceNormals();
|
||||||
|
return geom;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { NewEthereumGeometry };
|
2
web/js/keys.js
Normal file
2
web/js/keys.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
document.addEventListener("keydown", function(event) {
|
||||||
|
});
|
18
web/js/shitty_hacks.js
Normal file
18
web/js/shitty_hacks.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// accessorFn
|
||||||
|
function accessorFn(p) {
|
||||||
|
if (p instanceof Function) {
|
||||||
|
return p // fn
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof p === 'string') {
|
||||||
|
return function(obj) {
|
||||||
|
return obj[p]; // property name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (obj){
|
||||||
|
return p; // constant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = accessorFn;
|
26
web/js/ws.js
Normal file
26
web/js/ws.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
var graph = require('../index.js');
|
||||||
|
|
||||||
|
var ws = new WebSocket('ws://' + window.location.host + '/ws');
|
||||||
|
|
||||||
|
// request graphData and initial positions from websocket connection
|
||||||
|
ws.onopen = function (event) {
|
||||||
|
ws.send('{"cmd": "init"}');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function (event) {
|
||||||
|
let msg = JSON.parse(event.data);
|
||||||
|
switch(msg.type) {
|
||||||
|
case "graph":
|
||||||
|
graph.setGraphData(msg.graph);
|
||||||
|
break;
|
||||||
|
case "propagation":
|
||||||
|
graph.setPropagation(msg.propagation);
|
||||||
|
break;
|
||||||
|
case "positions":
|
||||||
|
console.log("Updating positions...");
|
||||||
|
graph.updatePositions(msg.positions);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { ws };
|
50
web/package-lock.json
generated
Normal file
50
web/package-lock.json
generated
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "clean",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-vHZD/KjlOoNH4vva/6I2eWtYUJs="
|
||||||
|
},
|
||||||
|
"d3-interpolate": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-mOnv5a+pZzkNIHtw/V6I+w9Lqm9L5bG3OTXPM5A+QO0yyVMQ4W1uZhR+VOJmazaOZXri2ppbiZ5BUNWT0pFM9A==",
|
||||||
|
"requires": {
|
||||||
|
"d3-color": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-scale-chromatic": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-qQUhLi8fPe/F0b0M46C6eFUbms5IIMHuhJ5DKjjzBUvm1b6aPtygJzGbrMdMUD/ckLBq+NdWwHeN2cpMDp4Q5Q==",
|
||||||
|
"requires": {
|
||||||
|
"d3-color": "1.0.3",
|
||||||
|
"d3-interpolate": "1.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dat.gui": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-fTv1JQqjwA1Rtjy/i0ThdLgUOSWA3hOJrvNEl3XrfaMvCcFdV0h01bo3aGXknLx3okJ7AO9qqgVz6TbuRHxYAw=="
|
||||||
|
},
|
||||||
|
"stats-js": {
|
||||||
|
"version": "1.0.0-alpha1",
|
||||||
|
"resolved": "https://registry.npmjs.org/stats-js/-/stats-js-1.0.0-alpha1.tgz",
|
||||||
|
"integrity": "sha1-RK6C5kIRI7q5l1DZXD9VJrjyV9s="
|
||||||
|
},
|
||||||
|
"three": {
|
||||||
|
"version": "0.88.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.88.0.tgz",
|
||||||
|
"integrity": "sha1-QlbC/Djk+yOg0j66K2zOTfjkZtU="
|
||||||
|
},
|
||||||
|
"tinycolor2": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
|
||||||
|
"integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
web/package.json
Normal file
18
web/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "clean",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-scale-chromatic": "^1.2.0",
|
||||||
|
"dat.gui": "^0.7.1",
|
||||||
|
"stats-js": "^1.0.0-alpha1",
|
||||||
|
"three": "^0.88.0",
|
||||||
|
"tinycolor2": "^1.4.1"
|
||||||
|
}
|
||||||
|
}
|
107
ws.go
Normal file
107
ws.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/divan/graph-experiments/graph"
|
||||||
|
"github.com/divan/graph-experiments/layout"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WSServer struct {
|
||||||
|
upgrader websocket.Upgrader
|
||||||
|
hub []*websocket.Conn
|
||||||
|
|
||||||
|
Positions []*position
|
||||||
|
layout layout.Layout
|
||||||
|
graph *graph.Graph
|
||||||
|
propagation *PropagationLog
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWSServer(layout layout.Layout) *WSServer {
|
||||||
|
ws := &WSServer{
|
||||||
|
upgrader: websocket.Upgrader{},
|
||||||
|
layout: layout,
|
||||||
|
}
|
||||||
|
ws.updatePositions()
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
type WSResponse struct {
|
||||||
|
Type MsgType `json:"type"`
|
||||||
|
Positions []*position `json:"positions,omitempty"`
|
||||||
|
Graph json.RawMessage `json:"graph,omitempty"`
|
||||||
|
Propagation *PropagationLog `json:"propagation,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WSRequest struct {
|
||||||
|
Cmd WSCommand `json:"cmd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MsgType string
|
||||||
|
type WSCommand string
|
||||||
|
|
||||||
|
// WebSocket response types
|
||||||
|
const (
|
||||||
|
RespPositions MsgType = "positions"
|
||||||
|
RespGraph MsgType = "graph"
|
||||||
|
RespPropagation MsgType = "propagation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebSocket commands
|
||||||
|
const (
|
||||||
|
CmdInit WSCommand = "init"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ws *WSServer) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := ws.upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
ws.hub = append(ws.hub, c)
|
||||||
|
|
||||||
|
for {
|
||||||
|
mt, message, err := c.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("read:", mt, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ws.processRequest(c, mt, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) processRequest(c *websocket.Conn, mtype int, data []byte) {
|
||||||
|
var cmd WSRequest
|
||||||
|
err := json.Unmarshal(data, &cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("unmarshal command", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd.Cmd {
|
||||||
|
case CmdInit:
|
||||||
|
ws.sendGraphData(c)
|
||||||
|
ws.updatePositions()
|
||||||
|
ws.sendPositions(c)
|
||||||
|
ws.sendPropagationData(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) sendMsg(c *websocket.Conn, msg *WSResponse) {
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("write:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.WriteMessage(1, data)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("write:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
37
ws_graph.go
Normal file
37
ws_graph.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/divan/graph-experiments/export"
|
||||||
|
"github.com/divan/graph-experiments/graph"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ws *WSServer) sendGraphData(c *websocket.Conn) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := export.NewJSON(&buf, false).ExportGraph(ws.graph)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Can't marshal graph to JSON")
|
||||||
|
}
|
||||||
|
msg := &WSResponse{
|
||||||
|
Type: RespGraph,
|
||||||
|
Graph: json.RawMessage(buf.Bytes()),
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.sendMsg(c, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) updateGraph(g *graph.Graph) {
|
||||||
|
ws.graph = g
|
||||||
|
|
||||||
|
ws.broadcastGraphData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) broadcastGraphData() {
|
||||||
|
for i := 0; i < len(ws.hub); i++ {
|
||||||
|
ws.sendGraphData(ws.hub[i])
|
||||||
|
}
|
||||||
|
}
|
41
ws_positions.go
Normal file
41
ws_positions.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/gorilla/websocket"
|
||||||
|
|
||||||
|
type position struct {
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Z int `json:"z"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) sendPositions(c *websocket.Conn) {
|
||||||
|
msg := &WSResponse{
|
||||||
|
Type: RespPositions,
|
||||||
|
Positions: ws.Positions,
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.sendMsg(c, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) updatePositions() {
|
||||||
|
// positions
|
||||||
|
nodes := ws.layout.Nodes()
|
||||||
|
positions := []*position{}
|
||||||
|
for i := 0; i < len(nodes); i++ {
|
||||||
|
pos := &position{
|
||||||
|
X: nodes[i].X,
|
||||||
|
Y: nodes[i].Y,
|
||||||
|
Z: nodes[i].Z,
|
||||||
|
}
|
||||||
|
positions = append(positions, pos)
|
||||||
|
}
|
||||||
|
ws.Positions = positions
|
||||||
|
|
||||||
|
ws.broadcastPositions()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) broadcastPositions() {
|
||||||
|
for i := 0; i < len(ws.hub); i++ {
|
||||||
|
ws.sendPositions(ws.hub[i])
|
||||||
|
}
|
||||||
|
}
|
26
ws_propagation.go
Normal file
26
ws_propagation.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ws *WSServer) sendPropagationData(c *websocket.Conn) {
|
||||||
|
msg := &WSResponse{
|
||||||
|
Type: RespPropagation,
|
||||||
|
Propagation: ws.propagation,
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.sendMsg(c, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) updatePropagationData(plog *PropagationLog) {
|
||||||
|
ws.propagation = plog
|
||||||
|
|
||||||
|
ws.broadcastPropagationData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WSServer) broadcastPropagationData() {
|
||||||
|
for i := 0; i < len(ws.hub); i++ {
|
||||||
|
ws.sendPropagationData(ws.hub[i])
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user