Initial commit

This commit is contained in:
Ivan Danyliuk 2018-07-04 18:56:18 +02:00
commit 7d860ad225
No known key found for this signature in database
GPG Key ID: 97ED33CE024E1DBF
29 changed files with 7847 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
whispervis
node_modules/
network.json
data.json
propagation.json

45
main.go Normal file
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

11
web/css/controls.css Normal file
View 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

File diff suppressed because one or more lines are too long

19
web/data/data.js Normal file
View File

@ -0,0 +1,19 @@
var graphData = {
"nodes": [
{
"id": "Foo",
"group": 1
},
{
"id": "Bar",
"group": 1
}
],
"links": [
{
"source": "Foo",
"target": "Bar",
"value": 1
}
]
};

View File

@ -0,0 +1 @@
["Foo","Bar"]

BIN
web/data/data/links.bin Normal file

Binary file not shown.

1
web/data/data/meta.json Normal file
View 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

Binary file not shown.

299
web/data/data_orig.json Normal file
View 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
View 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
View 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
View 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
View 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 };

View 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();
};

View 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
View 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
View File

@ -0,0 +1,2 @@
document.addEventListener("keydown", function(event) {
});

18
web/js/shitty_hacks.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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])
}
}