parent
4a7b82b2f8
commit
0d773e28b7
@ -0,0 +1,102 @@ |
||||
$button-color: rgb(147, 146, 146); |
||||
$main-color: rgb(240, 141, 25); |
||||
|
||||
*{ |
||||
margin:0; |
||||
padding: 0; |
||||
box-sizing: border-box; |
||||
} |
||||
body{ |
||||
font-family: Roboto; |
||||
|
||||
} |
||||
main{ |
||||
position: relative; |
||||
height: 100vh; |
||||
|
||||
#infobar{ |
||||
position: absolute; |
||||
top:0; |
||||
width: 100%; |
||||
padding: 0.4em; |
||||
background-color: rgba(232, 9, 9, 0.4); |
||||
border: 4px solid rgba(232, 9, 9, 0.8); |
||||
text-align: center; |
||||
font-weight: bold; |
||||
font-size: 1.8em; |
||||
z-index: 1; |
||||
transform: translateY(-100%); |
||||
transition: transform 1s; |
||||
&.active{ |
||||
transform: translateY(0%); |
||||
} |
||||
} |
||||
|
||||
.canvas{ |
||||
background-color: lightgrey; |
||||
position: relative; |
||||
height: 100%; |
||||
|
||||
input{ |
||||
position: absolute; |
||||
border: none; |
||||
padding: 0.6em 1em; |
||||
width: 50px; |
||||
transform: translateX(-50%) translateY(-50%); |
||||
} |
||||
} |
||||
|
||||
nav, footer{ |
||||
display: flex; |
||||
position: absolute; |
||||
top: 1em; |
||||
left: 1em; |
||||
opacity: 0.9; |
||||
|
||||
button{ |
||||
display: block; |
||||
background-color: $button-color; |
||||
border: none; |
||||
border-radius: 1em; |
||||
padding: 1em 1em; |
||||
min-width: 8em; |
||||
margin: 0.4em; |
||||
color: black; |
||||
border: solid 2px darken($button-color, 50); |
||||
box-shadow: 2px 2px 8px rgba(0,0,0,0.5); |
||||
|
||||
&:focus { |
||||
outline: none; |
||||
} |
||||
&::-moz-focus-inner { |
||||
border: 0; |
||||
} |
||||
&:hover{ |
||||
background-color: lighten($button-color, 10); |
||||
border-color: darken($button-color, 100); |
||||
cursor: pointer; |
||||
} |
||||
&.active{ |
||||
background-color: $main-color; |
||||
} |
||||
h1{ |
||||
text-align: center; |
||||
font-size: 1.4em; |
||||
font-weight: normal; |
||||
|
||||
white-space: nowrap |
||||
} |
||||
img{ |
||||
display: block; |
||||
margin: 0 auto 0.5em auto; |
||||
width: 2em; |
||||
} |
||||
} |
||||
} |
||||
footer{ |
||||
top: auto; |
||||
left: auto; |
||||
bottom: 1em; |
||||
right: 1em; |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
*{margin:0;padding:0;box-sizing:border-box}body{font-family:Roboto}main{position:relative;height:100vh}main #infobar{position:absolute;top:0;width:100%;padding:0.4em;background-color:rgba(232,9,9,0.4);border:4px solid rgba(232,9,9,0.8);text-align:center;font-weight:bold;font-size:1.8em;z-index:1;transform:translateY(-100%);transition:transform 1s}main #infobar.active{transform:translateY(0%)}main .canvas{background-color:lightgrey;position:relative;height:100%}main .canvas input{position:absolute;border:none;padding:0.6em 1em;width:50px;transform:translateX(-50%) translateY(-50%)}main nav,main footer{display:flex;position:absolute;top:1em;left:1em;opacity:0.9}main nav button,main footer button{display:block;background-color:#939292;border:none;border-radius:1em;padding:1em 1em;min-width:8em;margin:0.4em;color:black;border:solid 2px #131313;box-shadow:2px 2px 8px rgba(0,0,0,0.5)}main nav button:focus,main footer button:focus{outline:none}main nav button::-moz-focus-inner,main footer button::-moz-focus-inner{border:0}main nav button:hover,main footer button:hover{background-color:#acacac;border-color:#000;cursor:pointer}main nav button.active,main footer button.active{background-color:#f08d19}main nav button h1,main footer button h1{text-align:center;font-size:1.4em;font-weight:normal;white-space:nowrap}main nav button img,main footer button img{display:block;margin:0 auto 0.5em auto;width:2em}main footer{top:auto;left:auto;bottom:1em;right:1em} |
Binary file not shown.
After Width: | Height: | Size: 774 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,12 @@ |
||||
@font-face { |
||||
font-family: 'Roboto-Regular'; |
||||
src: url('Roboto-Regular.eot'); |
||||
src: url('Roboto-Regular.eot?#iefix') format('embedded-opentype'), |
||||
url('Roboto-Regular.svg#Roboto-Regular') format('svg'), |
||||
url('Roboto-Regular.ttf') format('truetype'), |
||||
url('Roboto-Regular.woff') format('woff'), |
||||
url('Roboto-Regular.woff2') format('woff2'); |
||||
font-weight: normal; |
||||
font-style: normal; |
||||
} |
||||
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,436 @@ |
||||
const canvas = document.getElementById("canvas"); |
||||
|
||||
// Layouts
|
||||
var width = canvas.offsetWidth; |
||||
var height = canvas.offsetHeight; |
||||
|
||||
const node_size = 55; |
||||
|
||||
var stage = new Konva.Stage({ |
||||
container: canvas, |
||||
width: width, |
||||
height: height |
||||
}); |
||||
|
||||
const node_layout = { |
||||
radius: node_size, |
||||
fill: 'white', |
||||
stroke: 'black', |
||||
strokeWidth: 2, |
||||
shadowColor: 'black', |
||||
shadowBlur: 10, |
||||
shadowOffset: { |
||||
x: 2, |
||||
y: 2 |
||||
}, |
||||
shadowOpacity: 0.3, |
||||
}; |
||||
|
||||
const node_text_layout = { |
||||
x: 1 - node_size, |
||||
y: 2 - node_size, |
||||
text: name, |
||||
fontSize: 22, |
||||
width: 2 * node_size-1, |
||||
height: 2 * node_size-1, |
||||
fontFamily: 'Roboto', |
||||
fill: 'black', |
||||
verticalAlign: 'middle', |
||||
align: 'center' |
||||
}; |
||||
|
||||
const edge_layout = { |
||||
stroke: 'black', |
||||
strokeWidth: 3, |
||||
} |
||||
|
||||
const edge_text_layout = { |
||||
fontSize: 22, |
||||
width: 2 * node_size, |
||||
height: 2 * node_size, |
||||
fontFamily: 'Roboto', |
||||
fill: 'black', |
||||
verticalAlign: 'middle', |
||||
align: 'center' |
||||
}; |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//-------------------------------Init------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
var edge_layer = new Konva.Layer(); |
||||
var node_layer = new Konva.Layer(); |
||||
|
||||
var konva_edges = []; |
||||
var konva_nodes = []; |
||||
var graph_copy = {}; |
||||
|
||||
var state = "idle"; |
||||
|
||||
// selected nodes
|
||||
var selected = []; |
||||
var path_elements = []; |
||||
|
||||
// (additions) auto layout
|
||||
var auto_layout = false; |
||||
|
||||
function initialisation(){ |
||||
stage.add(edge_layer); |
||||
stage.add(node_layer); |
||||
|
||||
// avoid flickering
|
||||
edge_layer.hide(); |
||||
|
||||
// draw initial graph
|
||||
for (id in nodes) { |
||||
draw_new_node(nodes[id], node_size + Math.random() * (width - 2 * node_size), node_size + Math.random() * (height - 2 * node_size)); |
||||
} |
||||
for (id in edges) { |
||||
draw_new_edge(edges[id]); |
||||
} |
||||
|
||||
// (additions) auto layout
|
||||
|
||||
if ( sessionStorage.getItem("auto_layout") !== null) { |
||||
// Restore the contents of the text field
|
||||
auto_layout = (sessionStorage.getItem("auto_layout") == 'true'); |
||||
if (auto_layout){ |
||||
document.querySelector("[data-activeon=auto_layout]").classList.add("active"); |
||||
} |
||||
} |
||||
// Precalculate a good graph presentation
|
||||
for (var i = 0; i < 1000; i++) { |
||||
force_directed_graph(); |
||||
} |
||||
node_layer.draw(); |
||||
|
||||
|
||||
edge_layer.show(); |
||||
|
||||
} |
||||
initialisation(); |
||||
//-----------------------------------------------------------------------------
|
||||
//------------------------------utils------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
function sort(array) { |
||||
return array.sort(function(a, b) { |
||||
return a - b; |
||||
}) |
||||
} |
||||
//-----------------------------------------------------------------------------
|
||||
//---------------------------draw methods--------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
function draw_new_node(name, x, y) { |
||||
const pos = { |
||||
x: x, |
||||
y: y |
||||
}; |
||||
let group = new Konva.Group({ |
||||
...pos, |
||||
draggable: true, |
||||
dragBoundFunc: function(pos) { |
||||
return { |
||||
x: sort([node_size, pos.x, width - node_size])[1], |
||||
y: sort([node_size, pos.y, height - node_size])[1], |
||||
}; |
||||
}, |
||||
id: name |
||||
}); |
||||
let box = new Konva.Circle(node_layout); |
||||
let simpleText = new Konva.Text({...node_text_layout, text: name}); |
||||
group.add(box); |
||||
group.add(simpleText); |
||||
group.on("click", on_node_click); |
||||
group.on('dragstart', function() { |
||||
group.attrs.dragging = true; |
||||
}); |
||||
group.on('dragend', function() { |
||||
group.attrs.dragging = false; |
||||
}); |
||||
group.on('mouseover', function() { |
||||
document.body.style.cursor = 'pointer'; |
||||
}); |
||||
group.on('mouseout', function() { |
||||
document.body.style.cursor = 'default'; |
||||
}); |
||||
graph_copy[name] = []; |
||||
konva_nodes.push(group); |
||||
|
||||
node_layer.add(group); |
||||
node_layer.draw(); |
||||
} |
||||
|
||||
|
||||
function draw_new_edge(edge) { |
||||
let start_node = stage.findOne('#' + edge["start"]); |
||||
let end_node = stage.findOne('#' + edge["end"]); |
||||
let weight = edge["weight"]; |
||||
let group = new Konva.Group({ |
||||
start_node: start_node, |
||||
end_node: end_node, |
||||
weight: weight, |
||||
id: edge["start"]+edge["end"] |
||||
}); |
||||
let line = new Konva.Line(edge_layout); |
||||
let simpleText = new Konva.Text({...edge_text_layout, text: weight}); |
||||
|
||||
graph_copy[edge["start"]].push(edge["end"]); |
||||
graph_copy[edge["end"]].push(edge["start"]); |
||||
|
||||
group.add(line); |
||||
group.add(simpleText); |
||||
konva_edges.push(group); |
||||
update_edge(group); |
||||
|
||||
edge_layer.add(group); |
||||
edge_layer.draw(); |
||||
|
||||
return group; |
||||
} |
||||
|
||||
function update_edge(konva_edge) { |
||||
let line = konva_edge.children[0]; |
||||
let text = konva_edge.children[1]; |
||||
let start_node = konva_edge.attrs.start_node; |
||||
let end_node = konva_edge.attrs.end_node; |
||||
let size = 15+text.text().length*15; |
||||
let line_length = Math.sqrt((start_node.getX() - end_node.getX()) ** 2 + (start_node.getY() - end_node.getY()) ** 2) - size; |
||||
line.points([start_node.getX(), start_node.getY(), end_node.getX(), end_node.getY()]); |
||||
line.dash([line_length / 2, size, line_length / 2]); |
||||
text.x((start_node.getX() + end_node.getX()) / 2 - node_size); |
||||
text.y((start_node.getY() + end_node.getY()) / 2 - node_size), |
||||
|
||||
edge_layer.draw(); |
||||
} |
||||
|
||||
function reset_node_style(){ |
||||
for (let id in konva_nodes) { |
||||
konva_nodes[id].children[0].strokeWidth(node_layout.strokeWidth); |
||||
konva_nodes[id].children[0].stroke("black"); |
||||
} |
||||
} |
||||
|
||||
function reset_edge_style(){ |
||||
for (let id in konva_edges) { |
||||
konva_edges[id].children[0].strokeWidth(edge_layout.strokeWidth); |
||||
konva_edges[id].children[0].stroke("black"); |
||||
} |
||||
} |
||||
|
||||
function draw_selected_nodes() { |
||||
reset_node_style(); |
||||
reset_edge_style(); |
||||
for (let id in selected) { |
||||
selected[id].children[0].strokeWidth(node_layout.strokeWidth + 1); |
||||
selected[id].children[0].stroke("rgb(240, 141, 25)"); |
||||
} |
||||
node_layer.draw(); |
||||
} |
||||
|
||||
function draw_path(path){ |
||||
let path_elements = []; |
||||
for (let id in path){ |
||||
path_elements.push(stage.findOne("#"+path[id])); |
||||
if (id == 0) { |
||||
continue |
||||
} |
||||
let current_edge = stage.findOne("#"+path[id-1]+path[id]); |
||||
if (current_edge){ |
||||
path_elements.push(current_edge); |
||||
}else{ |
||||
path_elements.push(stage.findOne("#"+path[id]+path[id-1])); |
||||
} |
||||
} |
||||
reset_node_style(); |
||||
reset_edge_style(); |
||||
for (let id in path_elements) { |
||||
path_elements[id].children[0].strokeWidth(node_layout.strokeWidth + 1); |
||||
path_elements[id].children[0].stroke("rgb(240, 141, 25)"); |
||||
} |
||||
} |
||||
|
||||
function on_node_click(e) { |
||||
e.cancelBubble = true; |
||||
// let mousePos = stage.getPointerPosition();
|
||||
let idx = selected.indexOf(this); |
||||
if (idx >= 0) { |
||||
selected.splice(idx, 1); |
||||
} else if (selected.length < 2) { |
||||
selected.push(this); |
||||
} else { |
||||
selected.shift(); |
||||
selected.push(this); |
||||
} |
||||
draw_selected_nodes(); |
||||
}; |
||||
|
||||
|
||||
|
||||
function create_node(){ |
||||
if (state != "create_node"){ |
||||
state = "create_node"; |
||||
// let mousePos = stage.getPointerPosition();
|
||||
let pos = {x: width/2, y: height/2}; |
||||
create_input(pos.x, pos.y).then(function(text) { |
||||
draw_new_node(text, pos.x, pos.y); |
||||
state = "idle"; |
||||
}, function(){state = "idle"}); |
||||
} |
||||
} |
||||
|
||||
|
||||
function create_edge() { |
||||
if (selected.length < 2) { |
||||
show_error("Select at least 2 nodes."); |
||||
return; |
||||
} |
||||
let x = (selected[0].getX() + selected[1].getX()) / 2; |
||||
let y = (selected[0].getY() + selected[1].getY()) / 2; |
||||
create_input(x, y).then(function(text) { |
||||
draw_new_edge({ |
||||
"start": selected[0].children[1].text(), |
||||
"end": selected[1].children[1].text(), |
||||
"weight": text |
||||
}); |
||||
selected = []; |
||||
draw_selected_nodes(); |
||||
},function(){}); |
||||
} |
||||
|
||||
|
||||
function create_input(x, y) { |
||||
return new Promise(function(resolve, reject) { |
||||
var input = document.createElement("input"); |
||||
input.setAttribute("type", "text"); |
||||
input.style.left = x + "px"; |
||||
input.style.top = y + "px"; |
||||
canvas.appendChild(input); |
||||
input.focus(); |
||||
stage.listening(false); |
||||
window.addEventListener("keyup", function(event) { |
||||
if (event.key === "Escape") { |
||||
input.remove(); |
||||
stage.listening(true); |
||||
reject(); |
||||
} |
||||
}); |
||||
input.addEventListener("keyup", function(event) { |
||||
if (event.key === "Enter") { |
||||
resolve(input.value); |
||||
input.remove(); |
||||
stage.listening(true); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
function find_path(){ |
||||
draw_path(elem); |
||||
} |
||||
|
||||
function update_edges(edges) { |
||||
for (id in edges) { |
||||
update_edge(edges[id]); |
||||
} |
||||
} |
||||
node_layer.on('beforeDraw', function() { |
||||
update_edges(konva_edges); |
||||
}); |
||||
|
||||
// adapt the stage on any window resize
|
||||
function fitStageIntoParentContainer() { |
||||
width = canvas.offsetWidth; |
||||
height = canvas.offsetHeight; |
||||
stage.width(canvas.offsetWidth); |
||||
stage.height(canvas.offsetHeight); |
||||
stage.draw(); |
||||
} |
||||
window.addEventListener('resize', fitStageIntoParentContainer); |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//---------------------(additions) auto layout---------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
function toggle_auto_layout() { |
||||
auto_layout = !auto_layout; |
||||
document.querySelector("[data-activeon=auto_layout]").classList.toggle("active"); |
||||
sessionStorage.setItem("auto_layout", auto_layout); |
||||
} |
||||
|
||||
function normalize(vec) { |
||||
let l = Math.sqrt(vec[0]**2+vec[1]**2); |
||||
return [vec[0]/l,vec[1]/l]; |
||||
} |
||||
|
||||
function in_edges(start_node, end_node){ |
||||
let idx = graph_copy[start_node.attrs.id].indexOf(end_node.attrs.id); |
||||
if (idx < 0){ |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
function force_directed_graph(spring_length = 300, step = 0.004){ |
||||
for (var idx in konva_nodes){ |
||||
if (konva_nodes[idx].attrs.dragging){ |
||||
continue |
||||
} |
||||
var x1 = konva_nodes[idx].getX(); |
||||
var y1 = konva_nodes[idx].getY(); |
||||
|
||||
var rep = [0,0]; |
||||
var attr = [0,0]; |
||||
for (let jdx in konva_nodes){ |
||||
if (idx == jdx){ |
||||
continue; |
||||
} |
||||
|
||||
let x2 = konva_nodes[jdx].getX(); |
||||
let y2 = konva_nodes[jdx].getY(); |
||||
let dir = normalize([x2-x1,y2-y1]); |
||||
let dist = ((x1-x2)**2+(y1-y2)**2); |
||||
if (in_edges(konva_nodes[idx], konva_nodes[jdx])){ |
||||
let f = (dist/spring_length); |
||||
attr[0] += f*dir[0]; |
||||
attr[1] += f*dir[1]; |
||||
} |
||||
let f = (spring_length**2/Math.sqrt(dist)); |
||||
rep[0] -= f*dir[0]; |
||||
rep[1] -= f*dir[1]; |
||||
} |
||||
let new_x = x1+step*(rep[0]+attr[0]); |
||||
let new_y = y1+step*(rep[1]+attr[1]); |
||||
if (new_x < 2*node_size) { |
||||
new_x += 10*step*((2*node_size-new_x)); |
||||
} else if (new_x > width-2*node_size) { |
||||
new_x += 10*step*((width-2*node_size-new_x)); |
||||
} |
||||
if (new_y < 2*node_size) { |
||||
new_y += 10*step*((2*node_size-new_y)); |
||||
} else if (new_y > height-2*node_size) { |
||||
new_y += 10*step*((height-2*node_size-new_y)); |
||||
} |
||||
konva_nodes[idx].x(new_x); |
||||
konva_nodes[idx].y(new_y); |
||||
} |
||||
} |
||||
|
||||
var anim = new Konva.Animation(function(frame) { |
||||
if (auto_layout) { |
||||
const spring_length = 300; |
||||
const step = 0.00025*frame.timeDiff; |
||||
force_directed_graph(spring_length, step) |
||||
} |
||||
}, node_layer); |
||||
anim.start(); |
||||
|
||||
|
||||
function show_error(msg){ |
||||
let infobar = document.querySelector("#infobar"); |
||||
infobar.innerHTML = msg; |
||||
infobar.classList.add("active"); |
||||
window.setTimeout(function(){ infobar.classList.remove("active"); }, 4000); |
||||
} |
||||
|
||||
// bug: this avoids glitches
|
||||
window.onfocus = function () {auto_layout = true;}; |
||||
window.onblur = function () {auto_layout = false;}; |
@ -0,0 +1,61 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="de" dir="ltr"> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title>Assignment</title> |
||||
<link rel="stylesheet" href="{{ url_for('static', filename='fonts/roboto.css') }}"> |
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.min.css') }}"> |
||||
<script src="https://unpkg.com/konva@4.0.7/konva.min.js"></script> |
||||
</head> |
||||
<body> |
||||
<main> |
||||
<div id="infobar">You have to select at least 2 nodes</div> |
||||
<div id="canvas" class="canvas"></div> |
||||
<nav> |
||||
<button type="button" onclick="create_node()"> |
||||
<img src="{{ url_for('static', filename='imgs/plus.svg') }}"> |
||||
<h1>Create</h1> |
||||
</button> |
||||
<button type="button" onclick="create_edge()"> |
||||
<img src="{{ url_for('static', filename='imgs/drawing.svg') }}"> |
||||
<h1>Connect</h1> |
||||
</button> |
||||
<button type="button" onclick="find_path()"> |
||||
<img src="{{ url_for('static', filename='imgs/share.svg') }}"> |
||||
<h1>Find Path</h1> |
||||
</button> |
||||
</nav> |
||||
<footer> |
||||
<button type="button" data-activeon="auto_layout" onclick="toggle_auto_layout()"> |
||||
<h1>Automatic Layout</h1> |
||||
</button> |
||||
</footer> |
||||
</main> |
||||
<script> |
||||
var nodes = ["Aachen", "Berlin", "Chemnitz", "Dresden", "Essen"] |
||||
var edges = [{ |
||||
"start": "Aachen", |
||||
"end": "Berlin", |
||||
"weight": 800 |
||||
},{ |
||||
"start": "Dresden", |
||||
"end": "Berlin", |
||||
"weight": 800 |
||||
},{ |
||||
"start": "Essen", |
||||
"end": "Berlin", |
||||
"weight": 800 |
||||
}, { |
||||
"start": "Chemnitz", |
||||
"end": "Dresden", |
||||
"weight": 5 |
||||
}, |
||||
{ |
||||
"start": "Chemnitz", |
||||
"end": "Berlin", |
||||
"weight": 5 |
||||
}] |
||||
</script> |
||||
<script src="{{ url_for('static', filename='js/canvas.js')}}"></script> |
||||
</body> |
||||
</html> |
@ -1,13 +0,0 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="de" dir="ltr"> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title>Assignment</title> |
||||
</head> |
||||
<body> |
||||
<p><b>nodes:</b> {{ nodes }}</p> |
||||
<p><b>edges:</b> {{ edges }}</p> |
||||
<div ></div> |
||||
<script src="{{ url_for('static', filename='js/canvas.js')}}"></script> |
||||
</body> |
||||
</html> |
Loading…
Reference in new issue