@@ -1,104 +1,130 @@ | |||
var generator = { | |||
links: [], | |||
nodes: [], | |||
selected: null, | |||
init: function () { | |||
this.actions.addNode.call(this, null, { | |||
x: 100, | |||
y: 100 | |||
x: 23, | |||
y: 14 | |||
}); | |||
renderer.center(this.nodes[0]); | |||
}, | |||
onClick: function (button, x, y) { | |||
var node = this.findNode(x, y); | |||
if (!node) | |||
return; | |||
if (button == 0) | |||
this.actions.addNode.call(this, node); | |||
else if (button == 1) | |||
this.actions.rotateNode.call(this, node); | |||
else if (button == 2) | |||
this.actions.extendNode.call(this, node); | |||
if (button == 0) { | |||
if (ui.shiftDown) | |||
this.actions.moveNode.call(this, x, y); | |||
else { | |||
this.actions.addNode.call(this, null, { | |||
x: x, | |||
y: y | |||
}); | |||
} | |||
} else if (button == 1) { | |||
var node = this.findNode(x, y); | |||
if (node) | |||
this.actions.selectNode.call(this, node); | |||
} else if (button == 2) { | |||
var node = this.findNode(x, y); | |||
if (node) | |||
this.actions.connectNode.call(this, node); | |||
} | |||
renderer.makeDirty(); | |||
}, | |||
findNode: function (x, y, nodes) { | |||
nodes = nodes || this.nodes; | |||
for (var i = 0; i < nodes.length; i++) { | |||
var n = nodes[i]; | |||
if (!( | |||
(n.pos.x > x) || | |||
(n.pos.y > y) || | |||
(n.pos.x + constants.blockSize <= x) | | |||
(n.pos.y + constants.blockSize <= y) | |||
)) | |||
return n; | |||
else { | |||
var f = this.findNode(x, y, n.children); | |||
if (f) | |||
return f; | |||
} | |||
} | |||
findNode: function (x, y) { | |||
return this.nodes.find(n => ((n.pos.x == x) && (n.pos.y == y))); | |||
}, | |||
actions: { | |||
addNode: function (parent, options = {}) { | |||
var nodes = this.nodes; | |||
selectNode: function (node) { | |||
if (this.selected) | |||
this.selected.selected = false; | |||
node.selected = true; | |||
this.selected = node; | |||
if (parent) | |||
options.angle = constants.defaultAngle; | |||
ui.setActive(node); | |||
}, | |||
addNode: function (parent, options = {}) { | |||
var node = tplNode.build({ | |||
id: nodes.length, | |||
angle: options.angle, | |||
id: this.nodes.length, | |||
x: options.x, | |||
y: options.y, | |||
parent: parent, | |||
distance: constants.defaultDistance | |||
y: options.y | |||
}); | |||
if (parent) | |||
parent.children.push(node); | |||
else | |||
nodes.push(node); | |||
this.nodes.push(node); | |||
}, | |||
rotateNode: function (node) { | |||
var newAngle = node.angle - constants.defaultAngleInc; | |||
node.parent.children.forEach(n => (n.angle = newAngle)); | |||
console.log(node.parent); | |||
connectNode: function (node) { | |||
if (this.selected) { | |||
if (ui.shiftDown) { | |||
this.links.spliceWhere(l => ( | |||
( | |||
(l.from == node) || | |||
(l.to == node) | |||
) && | |||
( | |||
(l.from == this.selected) || | |||
(l.to == this.selected) | |||
) && | |||
(node != this.selected) | |||
)); | |||
} else { | |||
this.links.push({ | |||
from: this.selected, | |||
to: node | |||
}); | |||
} | |||
this.selected = null; | |||
} else | |||
this.selected = node; | |||
}, | |||
moveNode: function (x, y) { | |||
if (!this.selected) | |||
return; | |||
this.selected.pos.x = x; | |||
this.selected.pos.y = y; | |||
}, | |||
extendNode: function (node) { | |||
node.distance += constants.defaultDistanceInc; | |||
recolorNode: function () { | |||
if (!this.selected) | |||
return; | |||
this.selected.color = (this.selected.color + 1) % 4; | |||
}, | |||
resizeNode: function () { | |||
if (!this.selected) | |||
return; | |||
this.selected.size = (this.selected.size + 1) % 3; | |||
} | |||
} | |||
}; | |||
var tplNode = { | |||
id: 0, | |||
children: [], | |||
color: 0, | |||
size: 0, | |||
pos: { | |||
x: 0, | |||
y: 0 | |||
}, | |||
build: function (options) { | |||
var res = $.extend(true, { | |||
parent: options.parent | |||
}, this, { | |||
var res = $.extend(true, {}, this, { | |||
id: this.id++, | |||
pos: { | |||
x: options.x, | |||
y: options.y | |||
}, | |||
distance: options.distance, | |||
angle: options.angle | |||
} | |||
}); | |||
delete res.build; | |||
@@ -107,6 +133,7 @@ var tplNode = { | |||
}; | |||
$(function () { | |||
ui.init(); | |||
renderer.init(); | |||
generator.init(); | |||
}) |
@@ -0,0 +1,65 @@ | |||
Array.prototype.firstIndex = function (callback, thisArg) { | |||
var T = thisArg; | |||
var O = Object(this); | |||
var len = O.length >>> 0; | |||
var k = 0; | |||
while (k < len) { | |||
var kValue; | |||
if (k in O) { | |||
kValue = O[k]; | |||
if (callback.call(T, kValue, k, O)) | |||
return k; | |||
} | |||
k++; | |||
} | |||
return -1; | |||
}; | |||
Array.prototype.spliceWhere = function (callback, thisArg) { | |||
var T = thisArg; | |||
var O = Object(this); | |||
var len = O.length >>> 0; | |||
var k = 0; | |||
while (k < len) { | |||
var kValue; | |||
if (k in O) { | |||
kValue = O[k]; | |||
if (callback.call(T, kValue, k, O)) { | |||
O.splice(k, 1); | |||
k--; | |||
} | |||
} | |||
k++; | |||
} | |||
}; | |||
Array.prototype.spliceFirstWhere = function (callback, thisArg) { | |||
var T = thisArg; | |||
var O = Object(this); | |||
var len = O.length >>> 0; | |||
var k = 0; | |||
while (k < len) { | |||
var kValue; | |||
if (k in O) { | |||
kValue = O[k]; | |||
if (callback.call(T, kValue, k, O)) { | |||
O.splice(k, 1); | |||
return kValue; | |||
} | |||
} | |||
k++; | |||
} | |||
}; |
@@ -6,8 +6,23 @@ | |||
<script src="../../src/client/plugins/jquery.min.js"></script> | |||
<script src="renderer.js"></script> | |||
<script src="generator.js"></script> | |||
<script src="ui.js"></script> | |||
<script src="helpers.js"></script> | |||
</head> | |||
<body> | |||
<canvas></canvas> | |||
<div class="left"> | |||
<canvas></canvas> | |||
</div> | |||
<div class="right"> | |||
<div class="id"></div> | |||
<div class="commands"> | |||
<div class="command">a: add nodes</div> | |||
<div class="command">e: extend nodes</div> | |||
<div class="command">r: rotate nodes</div> | |||
<div class="command">c: closen nodes</div> | |||
<div class="command">f: furthen nodes</div> | |||
<div class="command"></div> | |||
</div> | |||
</div> | |||
</body> | |||
</html> |
@@ -12,16 +12,24 @@ var renderer = { | |||
y: 0 | |||
}, | |||
mouse: { | |||
x: 0, | |||
y: 0 | |||
}, | |||
dirty: false, | |||
init: function () { | |||
this.canvas = $('canvas')[0]; | |||
this.screen.w = this.canvas.width = $('body').width(); | |||
this.screen.h = this.canvas.height = $('body').height(); | |||
this.screen.w = this.canvas.width = $('.left').width(); | |||
this.screen.h = this.canvas.height = $('.left').height(); | |||
this.ctx = this.canvas.getContext('2d'); | |||
this.ctx.lineWidth = constants.lineWidth; | |||
$(this.canvas) | |||
.on('mousedown', this.events.onClick.bind(this)) | |||
.on('mousemove', this.events.onMouseMove.bind(this)) | |||
.on('contextmenu', function () { | |||
return false; | |||
}); | |||
@@ -30,8 +38,8 @@ var renderer = { | |||
}, | |||
center: function (node) { | |||
this.pos.x = node.pos.x + (constants.blockSize / 2) - (this.screen.w / 2); | |||
this.pos.y = node.pos.y + (constants.blockSize / 2) - (this.screen.h / 2); | |||
this.pos.x = ~~(node.pos.x * constants.gridSize) + (constants.blockSize / 2) - (this.screen.w / 2); | |||
this.pos.y = ~~(node.pos.y * constants.gridSize) + (constants.blockSize / 2) - (this.screen.h / 2); | |||
this.ctx.translate(-this.pos.x, -this.pos.y); | |||
this.makeDirty(); | |||
@@ -41,32 +49,15 @@ var renderer = { | |||
this.dirty = true; | |||
}, | |||
render: function (nodes) { | |||
var nodes = nodes || generator.nodes; | |||
nodes.forEach(function (n) { | |||
var x = n.pos.x; | |||
var y = n.pos.y; | |||
if (n.parent) { | |||
var childIndex = n.parent.children.findIndex(c => (c == n)); | |||
n.pos.x = x = n.parent.pos.x + (Math.cos(n.angle * childIndex) * n.distance); | |||
n.pos.y = y = n.parent.pos.y + (Math.sin(n.angle * childIndex) * n.distance); | |||
} | |||
if (n.children.length > 0) | |||
this.render(n.children); | |||
n.children.forEach(function (c) { | |||
this.renderers.line.call(this, n, c); | |||
}, this); | |||
render: function () { | |||
var nodes = generator.nodes; | |||
var links = generator.links; | |||
links.forEach(function (l) { | |||
this.renderers.line.call(this, l.from, l.to); | |||
}, this); | |||
nodes.forEach(function (n) { | |||
if (n.children.length > 0) | |||
this.render(n.children); | |||
this.renderers.node.call(this, n, n.pos.x, n.pos.y); | |||
}, this); | |||
}, | |||
@@ -75,6 +66,7 @@ var renderer = { | |||
if (this.dirty) { | |||
this.dirty = false; | |||
this.renderers.clear.call(this); | |||
this.renderers.grid.call(this); | |||
this.render(); | |||
} | |||
@@ -86,19 +78,64 @@ var renderer = { | |||
this.ctx.clearRect(this.pos.x, this.pos.y, this.screen.w, this.screen.h); | |||
}, | |||
node: function (node, x, y) { | |||
this.ctx.fillStyle = '#c0c3cf'; | |||
this.ctx.fillRect(x, y, constants.blockSize, constants.blockSize) | |||
grid: function () { | |||
var gridSize = constants.gridSize; | |||
var ctx = this.ctx; | |||
var mouse = this.mouse; | |||
var w = this.screen.w / gridSize; | |||
var h = this.screen.h / gridSize; | |||
ctx.fillStyle = '#3c3f4c'; | |||
for (var i = 0; i < w; i++) { | |||
for (var j = 0; j < h; j++) { | |||
if ((mouse.x == i) && (mouse.y == j)) { | |||
ctx.fillStyle = '#ff6942'; | |||
ctx.fillRect((i * gridSize) - 25, (j * gridSize) - 25, 9, 9); | |||
ctx.fillStyle = '#3c3f4c'; | |||
} else | |||
ctx.fillRect((i * gridSize) - 23, (j * gridSize) - 23, 5, 5); | |||
} | |||
} | |||
}, | |||
node: function (node) { | |||
this.ctx.fillStyle = ([ | |||
'#c0c3cf', | |||
'#3fa7dd', | |||
'#4ac441', | |||
'#d43346' | |||
])[node.color]; | |||
var size = ([ | |||
constants.blockSize, | |||
constants.blockSize * 2, | |||
constants.blockSize * 3 | |||
])[node.size]; | |||
var x = (node.pos.x * constants.gridSize) - ((size - constants.blockSize) / 2); | |||
var y = (node.pos.y * constants.gridSize) - ((size - constants.blockSize) / 2); | |||
this.ctx.fillRect(x, y, size, size); | |||
if (node == generator.selected) { | |||
this.ctx.strokeStyle = '#fafcfc'; | |||
this.ctx.strokeRect(x, y, size, size); | |||
} | |||
}, | |||
line: function (fromNode, toNode) { | |||
var ctx = this.ctx; | |||
var halfSize = constants.blockSize / 2; | |||
var fromX = (fromNode.pos.x * constants.gridSize) + halfSize; | |||
var fromY = (fromNode.pos.y * constants.gridSize) + halfSize; | |||
var toX = (toNode.pos.x * constants.gridSize) + halfSize; | |||
var toY = (toNode.pos.y * constants.gridSize) + halfSize; | |||
ctx.strokeStyle = '#69696e'; | |||
ctx.beginPath(); | |||
ctx.moveTo(~~(fromNode.pos.x + halfSize) + 0.5, ~~(fromNode.pos.y + halfSize) + 0.5); | |||
ctx.lineTo(~~(toNode.pos.x + halfSize) + 0.5, ~~(toNode.pos.y + halfSize) + 0.5); | |||
ctx.moveTo(fromX, fromY); | |||
ctx.lineTo(toX, toY); | |||
ctx.closePath(); | |||
ctx.stroke(); | |||
} | |||
@@ -106,18 +143,33 @@ var renderer = { | |||
events: { | |||
onClick: function (e) { | |||
generator.onClick(e.button, e.clientX + this.pos.x, e.clientY + this.pos.y); | |||
generator.onClick(e.button, ~~((e.clientX + this.pos.x + 40) / constants.gridSize) - 1, ~~((e.clientY + this.pos.y + 40) / constants.gridSize) - 1); | |||
e.preventDefault(); | |||
return false; | |||
}, | |||
onMouseMove: function (e) { | |||
var mouseX = ~~((e.clientX + this.pos.x + 40) / constants.gridSize); | |||
var mouseY = ~~((e.clientY + this.pos.y + 40) / constants.gridSize); | |||
if ((this.mouse.x == mouseX) && (this.mouse.y == mouseY)) | |||
return; | |||
this.mouse = { | |||
x: mouseX, | |||
y: mouseY | |||
}; | |||
this.makeDirty(); | |||
} | |||
} | |||
}; | |||
var constants = { | |||
lineWidth: 5, | |||
blockSize: 40, | |||
defaultDistance: 100, | |||
defaultDistanceInc: 50, | |||
blockSize: 20, | |||
defaultDistance: 50, | |||
defaultDistanceInc: 60, | |||
defaultAngle: Math.PI / 2, | |||
defaultAngleInc: Math.PI / 8 | |||
defaultAngleInc: Math.PI / 8, | |||
gridSize: 30 | |||
}; |
@@ -8,3 +8,37 @@ body { | |||
margin: 0px; | |||
overflow: hidden; | |||
} | |||
.left, .right { | |||
float: left; | |||
height: 100%; | |||
} | |||
.left { | |||
width: 85%; | |||
} | |||
.right { | |||
width: 15%; | |||
background-color: #373041; | |||
} | |||
.right .id { | |||
width: 100%; | |||
text-align: center; | |||
padding: 10px; | |||
color: #fafcfc; | |||
font-size: 36px; | |||
} | |||
.right .command { | |||
width: 100%; | |||
text-align: center; | |||
padding: 10px; | |||
color: #fafcfc; | |||
font-size: 24px; | |||
} | |||
.right .id:before { | |||
content: 'id: '; | |||
} |
@@ -0,0 +1,37 @@ | |||
var ui = { | |||
node: null, | |||
shiftDown: false, | |||
init: function () { | |||
$(window) | |||
.on('keydown', this.events.onKey.bind(this, true)) | |||
.on('keyup', this.events.onKey.bind(this, false)); | |||
}, | |||
find: function (selector) { | |||
return $('.right').find('.' + selector).eq(0); | |||
}, | |||
setActive: function (node) { | |||
this.node = node; | |||
this.find('id').html(node.id); | |||
}, | |||
events: { | |||
onKey: function (isDown, e) { | |||
if (e.key == 'Shift') | |||
this.shiftDown = isDown; | |||
if (!isDown) | |||
return; | |||
if (e.key == 'c') { | |||
generator.actions.recolorNode.call(generator); | |||
renderer.makeDirty(); | |||
} else if (e.key == 's') { | |||
generator.actions.resizeNode.call(generator); | |||
renderer.makeDirty(); | |||
} | |||
} | |||
} | |||
}; |