I'm trying create a diagram like this using JointJS.
However, when I'm adding multiple links between elements, I'm only seeing 1 link show up. How do I go about adding multiple links with automatically adjusted space between them?
This is the code to add the boxes and links. Note that right now I'm just trying to add 3 links between all of the blocks, but I'm only seeing 1 link appear between each.
var steps = [{title: "Step 1"}, {title: "Step 2"}, {title: "Step 3"}];
steps.forEach(function(step, i){
var title = step.title;
var yOffset = i*150 + 50; //offsets first block by 50 in y and all others 150
var xOffset = 60; //offsets all blocks by 60
createBlock(title, xOffset, yOffset, i);
});
var blocks = [];
function createBlock(title, x, y, loc) {
var x = (typeof x !== 'undefined') ? x : 0;
var y = (typeof y !== 'undefined') ? y : 0;
var newBlock = new joint.shapes.html.Element({
position: { x: x, y: y },
size: { width: 170, height: 100 },
label: title,
attrs: {
'.label': {
text: title,
'ref-x': .5,
'ref-y': .4,
fill: '#FFFFFF'
},
}
});
blocks.push(newBlock.id);
graph.addCell(newBlock);
if(blocks.length > 1) {
var link = new joint.shapes.devs.Link({
source: {
id: blocks[loc-1],
},
target: {
id: blocks[loc],
},
});
graph.addCell(link);
var link2 = new joint.shapes.devs.Link({
source: {
id: blocks[loc-1],
},
target: {
id: blocks[loc],
},
});
graph.addCell(link2);
var link3 = new joint.shapes.devs.Link({
source: {
id: blocks[loc-1],
},
target: {
id: blocks[loc],
},
});
graph.addCell(link3);
}
}
all the links are lying on top of each other so you see it as a single one. There is code in the demo of jointjs to see each link in different paths. you could add the below code and see that the links show up in different paths. You will need to change the graph to your graph name in the below three lines
// displaying multiple links between two elements in different paths
function adjustVertices(graph, cell) {
// If the cell is a view, find its model.
cell = cell.model || cell;
if (cell instanceof joint.dia.Element) {
_.chain(graph.getConnectedLinks(cell)).groupBy(function(link) {
// the key of the group is the model id of the link's source or target, but not our cell id.
return _.omit([link.get('source').id, link.get('target').id], cell.id)[0];
}).each(function(group, key) {
// If the member of the group has both source and target model adjust vertices.
if (key !== 'undefined') adjustVertices(graph, _.first(group));
});
return;
}
// The cell is a link. Let's find its source and target models.
var srcId = cell.get('source').id || cell.previous('source').id;
var trgId = cell.get('target').id || cell.previous('target').id;
// If one of the ends is not a model, the link has no siblings.
if (!srcId || !trgId) return;
var siblings = _.filter(graph.getLinks(), function(sibling) {
var _srcId = sibling.get('source').id;
var _trgId = sibling.get('target').id;
return (_srcId === srcId && _trgId === trgId) || (_srcId === trgId && _trgId === srcId);
});
switch (siblings.length) {
case 0:
// The link was removed and had no siblings.
break;
case 1:
// There is only one link between the source and target. No vertices needed.
cell.unset('vertices');
break;
default:
// There is more than one siblings. We need to create vertices.
// First of all we'll find the middle point of the link.
var srcCenter = graph.getCell(srcId).getBBox().center();
var trgCenter = graph.getCell(trgId).getBBox().center();
var midPoint = joint.g.line(srcCenter, trgCenter).midpoint();
// Then find the angle it forms.
var theta = srcCenter.theta(trgCenter);
// This is the maximum distance between links
var gap = 20;
_.each(siblings, function(sibling, index) {
// We want the offset values to be calculated as follows 0, 20, 20, 40, 40, 60, 60 ..
var offset = gap * Math.ceil(index / 2);
// Now we need the vertices to be placed at points which are 'offset' pixels distant
// from the first link and forms a perpendicular angle to it. And as index goes up
// alternate left and right.
//
// ^ odd indexes
// |
// |----> index 0 line (straight line between a source center and a target center.
// |
// v even indexes
var sign = index % 2 ? 1 : -1;
var angle = joint.g.toRad(theta + sign * 90);
// We found the vertex.
var vertex = joint.g.point.fromPolar(offset, angle, midPoint);
sibling.set('vertices', [{ x: vertex.x, y: vertex.y }]);
});
}
};
var myAdjustVertices = _.partial(adjustVertices, graph);
// adjust vertices when a cell is removed or its source/target was changed
graph.on('add remove change:source change:target', myAdjustVertices);
// also when an user stops interacting with an element.
graph.on('cell:pointerup', myAdjustVertices);
The core of the solution lies in the adjustVertices function presented below. It accepts a graph and a cell (link or element). For added convenience, the function accepts cell views as well as models.
If cell is a link, it will find all links with the same source and target and then set vertices on them; we will be calling those related links 'siblings'.
If cell is an element, we execute our function for each distinct (different source and target) link connected to the element.
function adjustVertices(graph, cell) {
// if `cell` is a view, find its model
cell = cell.model || cell;
if (cell instanceof joint.dia.Element) {
// `cell` is an element
_.chain(graph.getConnectedLinks(cell))
.groupBy(function(link) {
// the key of the group is the model id of the link's source or target
// cell id is omitted
return _.omit([link.source().id, link.target().id], cell.id)[0];
})
.each(function(group, key) {
// if the member of the group has both source and target model
// then adjust vertices
if (key !== 'undefined') adjustVertices(graph, _.first(group));
})
.value();
return;
}
// `cell` is a link
// get its source and target model IDs
var sourceId = cell.get('source').id || cell.previous('source').id;
var targetId = cell.get('target').id || cell.previous('target').id;
// if one of the ends is not a model
// (if the link is pinned to paper at a point)
// the link is interpreted as having no siblings
if (!sourceId || !targetId) return;
// identify link siblings
var siblings = _.filter(graph.getLinks(), function(sibling) {
var siblingSourceId = sibling.source().id;
var siblingTargetId = sibling.target().id;
// if source and target are the same
// or if source and target are reversed
return ((siblingSourceId === sourceId) && (siblingTargetId === targetId))
|| ((siblingSourceId === targetId) && (siblingTargetId === sourceId));
});
var numSiblings = siblings.length;
switch (numSiblings) {
case 0: {
// the link has no siblings
break;
} case 1: {
// there is only one link
// no vertices needed
cell.unset('vertices');
break;
} default: {
// there are multiple siblings
// we need to create vertices
// find the middle point of the link
var sourceCenter = graph.getCell(sourceId).getBBox().center();
var targetCenter = graph.getCell(targetId).getBBox().center();
var midPoint = g.Line(sourceCenter, targetCenter).midpoint();
// find the angle of the link
var theta = sourceCenter.theta(targetCenter);
// constant
// the maximum distance between two sibling links
var GAP = 20;
_.each(siblings, function(sibling, index) {
// we want offset values to be calculated as 0, 20, 20, 40, 40, 60, 60 ...
var offset = GAP * Math.ceil(index / 2);
// place the vertices at points which are `offset` pixels perpendicularly away
// from the first link
//
// as index goes up, alternate left and right
//
// ^ odd indices
// |
// |----> index 0 sibling - centerline (between source and target centers)
// |
// v even indices
var sign = ((index % 2) ? 1 : -1);
// to assure symmetry, if there is an even number of siblings
// shift all vertices leftward perpendicularly away from the centerline
if ((numSiblings % 2) === 0) {
offset -= ((GAP / 2) * sign);
}
// make reverse links count the same as non-reverse
var reverse = ((theta < 180) ? 1 : -1);
// we found the vertex
var angle = g.toRad(theta + (sign * reverse * 90));
var vertex = g.Point.fromPolar(offset, angle, midPoint);
// replace vertices array with `vertex`
sibling.vertices([vertex]);
});
}
}
}
We then attach the necessary event listeners (function bindInteractionEvents). The vertices are recalculated anytime the user translates an element - as well as anytime a link is added/removed or has its source or target changed.
function bindInteractionEvents(adjustVertices, graph, paper) {
// bind `graph` to the `adjustVertices` function
var adjustGraphVertices = _.partial(adjustVertices, graph);
// adjust vertices when a cell is removed or its source/target was changed
graph.on('add remove change:source change:target', adjustGraphVertices);
// adjust vertices when the user stops interacting with an element
paper.on('cell:pointerup', adjustGraphVertices);
}
Related
i have written an algorithm that finds each line of a hexagon in a huge object-array structure.
the array consits of about 80.000 - 100.000 elements (line coordinates from start to end).
A hexagon cosists of 6 lines points. So the array has the information of about 15.000 hexagons.
The structure of the object (UNSORTED!!!) looks like this:
const stamps = [
{
vertices: [
{x: 114.5116411118, y: 342.9815785601},
{x: 115.6663416502, y: 344.9815785601}
]
},
{
vertices: [
{x: 115.6663416502, y: 340.9815785601},
{x: 114.5116411118, y: 342.9815785601}
]
},
{
vertices: [
{x: 122.6663416502, y: 364.9815785601},
{x: 147.9757427269, y: 314.9815785601},
]
},
{
vertices: [
{x: 117.9757427269, y: 340.9815785601},
{x: 115.6663416502, y: 340.9815785601},
]
},
{
vertices: [
{x: 119.1304432653, y: 342.9815785601},
{x: 117.9757427269, y: 340.9815785601},
]
},
{
vertices: [
{x: 117.9757427269, y: 344.9815785601},
{x: 119.1304432653, y: 342.9815785601},
]
},
{
vertices: [
{x: 115.6663416502, y: 344.9815785601},
{x: 117.9757427269, y: 344.9815785601},
]
},
];
To find each line hexagon, my idea was that there has to be 2 elements that have to same coordinate. If this is the case, i'm jumping to the index of this element and repeat that process untill i have all 6 lines of the hexagon.
It works like this, but its really, really slow. For an array with 80.000 elements its about 3 minutes.
The algorithm:
function findHexPolyPoints() {
const hexCoordinates = [];
let activeArrayPos = 0;
let position = 0;
while (1) {
let foundPair = false;
if (stamps.length < 6) break;
for (let k = 0; k < stamps.length; ++k) {
if (k === position) continue;
if (stamps[position].vertices[0].x === stamps[k].vertices[1].x && stamps[position].vertices[0].y === stamps[k].vertices[1].y) {
if (hexCoordinates[activeArrayPos]) {
hexCoordinates[activeArrayPos].push(stamps[k].vertices[0].x, stamps[k].vertices[0].y);
} else {
hexCoordinates.push([stamps[position].vertices[0].x, stamps[position].vertices[0].y, stamps[k].vertices[0].x, stamps[k].vertices[0].y]);
}
foundPair = true;
} else if (stamps[position].vertices[1].x === stamps[k].vertices[0].x && stamps[position].vertices[1].y === stamps[k].vertices[0].y) {
if (hexCoordinates[activeArrayPos]) {
hexCoordinates[activeArrayPos].push(stamps[k].vertices[1].x, stamps[k].vertices[1].y);
} else {
hexCoordinates.push([stamps[position].vertices[1].x, stamps[position].vertices[1].y, stamps[k].vertices[1].x, stamps[k].vertices[1].y]);
}
foundPair = true;
}
if (foundPair) {
stamps.splice(position, 1);
if (k > position) {
position = k - 1;
} else {
position = k;
}
if (hexCoordinates[activeArrayPos].length < 12) break;
}
if (hexCoordinates[activeArrayPos] && hexCoordinates[activeArrayPos].length === 12) {
if (k > position) stamps.splice(k - 1, 1);
else stamps.splice(k, 1);
activeArrayPos += 1;
position = 0;
break;
}
if (k === stamps.length - 1) {
stamps.splice(position, 1);
break;
}
}
}
sortHexagons(hexCoordinates);
}
Is there any way to speed up my algorithm? I have read that a simple for loop is still faster that some js sort functions like .map .filter or similar.
The following O(n) algorithm assumes
two different hexagons do not have overlapping vertices
there are no more than two same vertices in the array (meaning, there could be an orphan that doesn't belong to any hexagon, but its coordinates should not equal any of the hexagons vertices)
there is no floating point inaccuracy in coordinates (meaning two vertices that are supposed to be equal, are === exactly equal)
6 connected vertices are assumed to form an hexagon... (there is no barycenter calculation and checks to ensure it's actually an hexagon)
(if the points 1. and 2. are not true, the algo would need to be worked more, to try all possibilities (in overt[x_y] array, see below) in order to avoid non-hexagon or overlapping coords, depending on the expectancy to find overlapping hexagons, or orphans, the complexity could go beyond O(n))
Using the concept of map (get an object value from a key), which is considered O(1).
In order to conveniently use the vertices coordinates, we can concatenate x and y into one string
x:123.456, y:789.321
gives
x_y = "123.456_789.321"
Let's create 3 variables avert = [], overt = {}, hexas = []
avert is an array of all vertices, avert[index] is an array(2) of the x_y vertices coordinates
overt is an object that, for each x_y coordinates gives an array of indexes in avert (the size should not become more than 2, as assumed above (and there is no >2 check))
hexas is an array of array(6), the list of hexagons found (each coordinate is formatted x_y)
In the first forEach, avert and overt are created.
The next forEach processes all avert vertices [x_y1, x_y2]
starting from the first vertex, tries to find the 6 points to form an hexagon
adds each vertex to one hexagon array hexa, starting from the next (after the first)
assume coordinates are not sorted, thus ensure we're not going back to previous vertice
skips the used vertices (in hexagons)
ensure the last vertex found has the same coordinates as the origin (first)
Initialization
let avert = [], overt = {}, hexas = [];
stamps.forEach(function(e, i){
let xy1 = e['vertices'][0]['x']+'_'+e['vertices'][0]['y'];
let xy2 = e['vertices'][1]['x']+'_'+e['vertices'][1]['y'];
// overt[XY] (array) should have two elements at most (no overlapping),
// one if orphan
if ( ! overt[xy1]) overt[xy1] = [];
overt[xy1].push( i );
if ( ! overt[xy2]) overt[xy2] = [];
overt[xy2].push( i );
avert.push([ xy1, xy2 ]);
});
Processing
avert.forEach(function (e){
let j,coord = e[0]; // first coords x_y
let origin = coord;
let hexa = [];
let lastindex = -1; // avoid going back!
// try to find 5 connected vertices + origin
for(j=0 ; j<6 ; j++) {
let o = overt[coord];
if ( o === undefined || o.length < 2 ) {
break; // not found(already processed), or orphan!
}
let index = o[0] == lastindex ? o[1] : o[0]; // no turn back!
lastindex = index;
coord = avert[index][0] === coord ? avert[index][1] : avert[index][0];
hexa.push(coord);
}
if (j >= 6) { // found all vertices
// check that the last coord is the starting point
if (hexa[5] === origin) { // got it
hexas.push( hexa );
hexa.forEach(function(h){ // delete used vertices
delete overt[h];
});
}
}
});
All hexagons should be in hexas
You can avoid the nested loop over all data by using a hash map. Key the individual vertices with a hash, for instance their JSON representation, and store the corresponding x, y coordinate together with a list of neigbor objects.
Once you have that, it is easy to walk through that graph and identify the hexagons.
Runnable snippet using the sample data you provided:
function findHexPolyPoints(stamps) {
// Create graph
let map = new Map;
for (let {vertices} of stamps) {
// Get unique identifier for each vertex (its JSON notation)
let keys = vertices.map(JSON.stringify);
// Create "nodes" for each vertex, keyed by their key
let nodes = keys.map(function (key, i) {
let {x, y} = vertices[i];
let node = map.get(key);
if (!node) map.set(key, node = { key, x, y, neighbors: [] });
return node;
});
// Link these two nodes in both directions
nodes[0].neighbors.push(nodes[1]);
nodes[1].neighbors.push(nodes[0]);
}
// Walk through the graph to detect and collect hexagons
let hexagons = [];
for (let [key, vertex] of map) {
let hexagon = [];
while (vertex && hexagon.push(vertex) < 6) {
vertex = vertex.neighbors.find(neighbor => !hexagon.includes(neighbor));
}
// Remove collected nodes so they don't get visited a second time
for (let {key} of hexagon) map.delete(key);
// Verify that they form indeed a hexagon:
if (vertex && vertex.neighbors.includes(hexagon[0])) {
// Simplify the hexagon to only coordinates (12 coordinates)
hexagons.push(hexagon.flatMap(({x, y}) => [x, y]));
}
}
return hexagons;
}
// Demo. Just replace `stamps` with your actual data.
const stamps = [{vertices: [{x: 114.5116411118, y: 342.9815785601},{x: 115.6663416502, y: 344.9815785601}]},{vertices: [{x: 115.6663416502, y: 340.9815785601},{x: 114.5116411118, y: 342.9815785601}]},{vertices: [{x: 122.6663416502, y: 364.9815785601},{x: 147.9757427269, y: 314.9815785601},]},{vertices: [{x: 117.9757427269, y: 340.9815785601},{x: 115.6663416502, y: 340.9815785601},]},{vertices: [{x: 119.1304432653, y: 342.9815785601},{x: 117.9757427269, y: 340.9815785601},]},{vertices: [{x: 117.9757427269, y: 344.9815785601},{x: 119.1304432653, y: 342.9815785601},]},{vertices: [{x: 115.6663416502, y: 344.9815785601},{x: 117.9757427269, y: 344.9815785601},]},];
let hexagons = findHexPolyPoints(stamps);
console.log(hexagons);
It is true that plain old for loops are somewhat faster than .map, .forEach, .reduce, .find and the likes, but here I kept using them, as the main speed up is really coming from using a hash map.
I'm looking to be able to link the x-labels in a Chart.js bar chart. I've searched pretty thoroughly, and ended up trying to come up with my own solution: because the labels correspond to the bars directly above them and Chart.js has a built in getBarsAtEvent(evt) method, I tried creating an event if the user didn't click on a chart - this new event had pageX and pageY that was directly above the initial click such that if the user had clicked on a label, the new event would simulate a click on the bar graph.
However, calling getBarsAtEvent(createdClickEvent) repeatedly gives me a Uncaught TypeError ("Cannot read property 'getBoundingClientRect' of null"), which must mean that the getBarsAtEvent method, when called on my simulated click, isn't actually returning anything.
Any suggestions or alternate approaches would be greatly appreciated, thanks in advance.
An alternative approach would be to determine the point where the user is actually clicked and based on that calculate which label was clicked. For that you will need some information about the chart created and have to do some calculations.
Below is a way of doing that, and here is a Fiddle with this code/approach. Hope it helps.
$("#canvas").click(
function(evt){
var ctx = document.getElementById("canvas").getContext("2d");
// from the endPoint we get the end of the bars area
var base = myBar.scale.endPoint;
var height = myBar.chart.height;
var width = myBar.chart.width;
// only call if event is under the xAxis
if(evt.pageY > base){
// how many xLabels we have
var count = myBar.scale.valuesCount;
var padding_left = myBar.scale.xScalePaddingLeft;
var padding_right = myBar.scale.xScalePaddingRight;
// calculate width for each label
var xwidth = (width-padding_left-padding_right)/count;
// determine what label were clicked on AND PUT IT INTO bar_index
var bar_index = (evt.offsetX - padding_left) / xwidth;
// don't call for padding areas
if(bar_index > 0 & bar_index < count){
bar_index = parseInt(bar_index);
// either get label from barChartData
console.log("barChartData:" + barChartData.labels[bar_index]);
// or from current data
var ret = [];
for (var i = 0; i < myBar.datasets[0].bars.length; i++) {
ret.push(myBar.datasets[0].bars[i].label)
};
console.log("current data:" + ret[bar_index]);
// based on the label you can call any function
}
}
}
);
I modified iecs's answer to work with chartjs 2.7.1
var that = this;
this.chart = new Chart($("#chart"), {
type: 'bar',
data: {
labels: labels,
datasets: datasets
},
options: {
events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"],
onClick: function(e, data) {
var ctx = $("#chart")[0].getContext("2d");
var base = that.chart.chartArea.bottom;
var height = that.chart.chart.height;
var width = that.chart.chart.scales['x-axis-0'].width;
var offset = $('#chart').offset().top - $(window).scrollTop();
if(e.pageY > base + offset){
var count = that.chart.scales['x-axis-0'].ticks.length;
var padding_left = that.chart.scales['x-axis-0'].paddingLeft;
var padding_right = that.chart.scales['x-axis-0'].paddingRight;
var xwidth = (width-padding_left-padding_right)/count;
// don't call for padding areas
var bar_index = (e.offsetX - padding_left - that.chart.scales['y-axis-0'].width) / xwidth;
if(bar_index > 0 & bar_index < count){
bar_index = Math.floor(bar_index);
console.log(bar_index);
}
}
}
}
});
The main differences are:
The newer versions of chartjs use an chart.scales array instead of chart.scale with a bunch of values
I had to subtract chart.scales['y-axis-0'].width from the x offset to get the correct bar_index
I changed parseInt to Math.floor, just personal preference
And if you want the cursor to change when you hover over them, add "hover" to the events array and this to the options:
onHover: function(e) {
var ctx = $("#chart")[0].getContext("2d");
var base = that.chart.chartArea.bottom;
var height = that.chart.chart.height;
var width = that.chart.chart.scales['x-axis-0'].width;
var yOffset = $('#chart').offset().top - $(window).scrollTop();
var xOffset = $('#chart').offset().left - $(window).scrollLeft();
var left = xOffset + that.chart.scales['x-axis-0'].paddingLeft + that.chart.scales['x-axis-0'].left;
var right = xOffset + that.chart.scales['x-axis-0'].paddingRight + that.chart.scales['x-axis-0'].left + width;
if(e.pageY > base + yOffset && e.pageX > left && e.pageX < right){
e.target.style.cursor = 'pointer';
}
else {
e.target.style.cursor = 'default';
}
}
I'm developing a tool to add various sprites to the stage. When I drag an element I'd like to display o bounding box ( a rectangle ) that need to move accordingly to the dragging item.
To handle the drag functionality I'm using a lib called draggable
This is the constructor of every single object I push on the stage:
function createElement(x, y, ass_id)
{
// create our little bunny friend..
bunny = new PIXI.Sprite(textures[ass_id]);
bunny.scale.x = bunny.scale.y = 0.2;
bunny.draggable({
snap: true,
snapTolerance:0,
grid: [ 50, 50 ],
alpha: 0.5,
mousedown: function(data) {
/*var fishBounds = new PIXI.Rectangle(
-fishBoundsPadding,
-fishBoundsPadding,
viewWidth + fishBoundsPadding * 2,
viewHeight + fishBoundsPadding * 2);*/
texture_w = (data.target.texture.width) * data.target.scale.x;
texture_h = (data.target.texture.height) * data.target.scale.y;
// scale = data.target.scale.x;
var box = new PIXI.Graphics();
box.lineStyle(2, 0x666666);
box.drawRect(data.target.position.x, data.target.position.y, texture_w, texture_h);
box.scale.x = box.scale.y = scale;
stage.addChild(box);
data.target.boundingBox = box;
console.log(data.target.boundingBox.position, data.target.position);
},
drag: function(data) {
offset_x = data.boundingBox.position.x;//data.position;
offset_y = data.boundingBox.position.y;
data.boundingBox.position.x = data.position.x;// * data.scale.x;// - offset_x;
data.boundingBox.position.y = data.position.y;// * data.scale.y;// - offset_y;
console.log(stage.children.length , data.boundingBox.position, data.position, data);
},
mouseup: function(data) {
console.log("drop");
stage.removeChild(data.target.boundingBox);
}
});
// move the sprite to its designated position
bunny.position.x = x;
bunny.position.y = y;
elements.push(bunny);
// add it to the stage
stage.addChild(elements[elements.length-1]);
}
now, this works like a charm: when I click the element a bounding box gets created in the correct location, the problem is that when I start drag it around the bounding box get away from the item. I thing that the reason for this might be due to the fact the one item is scaled, wether the other isn't, but since I'm a noob at pixi I really find myself stuck with it.
Ok, I discovered that you can easily and conveniently attach an object to another via the addChild, so it comes out like this:
function createElement(x, y, ass_id)
{
// create our little bunny friend..
bunny = new PIXI.Sprite(textures[ass_id]);
bunny.scale.x = bunny.scale.y = 0.2;
bunny.draggable({
snap: true,
snapTolerance:0,
grid: [ 50, 50 ],
alpha: 0.5,
mousedown: function(data) {
texture_w = (data.target.texture.width);
texture_h = (data.target.texture.height);
var box = new PIXI.Graphics();
box.lineStyle(5, 0x666666);
box.drawRect(0, 0, texture_w, texture_h);
data.target.type = "element";
data.target.addChild(box);
},
drag: function(data) {
},
mouseup: function(data) {
console.log("drop");
for (var i = stage.children.length - 1; i >= 0; i--) {
if((stage.children[i].type) && (stage.children[i].type == "element"))
for (var j = stage.children[i].length - 1; i >= 0; i--) {
console.log('remove boundingBox child here when needed');
}
};
}
});
// move the sprite to its designated position
bunny.position.x = x;
bunny.position.y = y;
elements.push(bunny);
// add it to the stage
stage.addChild(elements[elements.length-1]);
}
I am building 2D tile game map. Each tile is a div in 2D array (var tiles = []). Below is the function which builds a tile based on some arguments defined somewhere else:
function Tile(rnd, px, py, nid) {
var self = this;
var _types = ["grass","forest","hills","swamp","forest", "mountains"];
var height = 60;
var width = 60;
var tileID = nid // new numeric tile id
var id = "tile_"+px+py+rnd; // old tile ID
var x = px;
var y = py;
var type = _types[rnd];
var img = 'img/maptiles/'+type+'.png';
this.Draw = function() {
var div = $("<div class='tile'></div>");
div.attr('id',tileID).data('type',type).data('x',x).data('y',y);
div.get(0).tile = self;
div.css({top:height*y, left:width*x});
div.css({"background":"url('"+img+"')"});
div.appendTo('#map-content');
};
this.Alert = function() {
alert("Tile type: "+type+". Tile ID: "+tileID+" ");
};
this.Move = function(){ // moves player to available tile, in this case if player stands to a tile before the clicked one
alert("start move! Tile type: "+type+". Tile ID: "+tileID+" ");
if (playerPosition === tileID - 1) {
$("#player").remove();
$("#????")").append('<img id="player" src="Pictures/maptiles/player.png" />');
playerPosition = tileID;
alert("Player position now: " + playerPosition);
}
};
}
As a result I end up with m x n map made of divs each with a unique numeric ID starting with 1. I know that using numeric IDs for elements is(was?) frowned upon, though HTML5 specs do not actually prohibit this anymore.
Now what I want to do is to place a player icon (player.png) depending on player's position. Starting position is 1 and I am using tiles (aka divs) numeric IDs to calculate legal moves (can move only to bordering tiles).
When a tile is clicked this.Move is called. Its purpose is to check if player can move on a clicked tile (just one IF statement for now to test) and must remove player.png and redraw it on the clicked div.
Here I run into a problem since I need to somehow use the tileID (which is equal to Div ID) to tell browser to append the DIV which is belong clicked (as I obviously do not write a function for every div on the field). I think that since I can get the DIV id on click I can use it somehow.
I tried to experiment with this.div.id:eq("+tileID+") but with no luck.
UPD:
Adding click handler as requested. Note this is within var Map, which is responsible for building the map, rendering and handling some user input:
var Map = new function() {
var maxHorz = 20;
var maxVert = 5;
var tiles = [];
this.init = function() {
for(var i=0; i<maxVert; i++) {
tiles[i] = [];
for(var j=0; j<maxHorz; j++) {
tiles[i][j] = new Tile(Math.random()*6|0, j, i, tileID++);
}
}
Render();
Setup();
};
this.GetMap = function() {
return tiles;
};
var Render = function() {
$.each(tiles, function(k,v){
$.each(v, function(k,t){
t.Draw();
});
});
};
var Setup = function(){
$('#map-content').on('click','div.tile', function(e){
//var tile = tiles[$(this).data('y')][$(this).data('x')];
this.tile.Move();
});
}
this.Redraw = function(x,y) {
};
}
Map.init();
The answer is found, finally :)
$('#player').detach().appendTo($('#'+tileID))
Here is the fiddle: http://jsfiddle.net/sw31uokt/
Here is some of the relevant code for the incrementValue function I set up to count overall clicks within the canvas element.
What I would like to do is be able to display a count of each color, so "you have placed 14 red pixels, 3 blue pixels, 4 black pixels'.
function incrementValue()
{
var value = parseInt(document.getElementById('number').value, 10);
value = isNaN(value) ? 0 : value;
value++;
document.getElementById('number').value = value;
}
$(c_canvas).click(function(evt) {
var pos = getNearestSquare(getMousePos(c_canvas, evt));
if (pos != null) {
context.fillStyle=(currentColor);
context.fillRect(pos.x,pos.y,19,19);
incrementValue();
}
});
Basically, what MarkE said above ...
In the outer scope, add two new vars :
var palette = ["333333", "0000ff", "a0522d", "46ad42", "808080", "ffc0cb", "d73952", "ffe2a8", "ffff7d", "ffffff"];//as originally defined in the .spectrum() call.
var gridModel = [];//Eventually a sparse array of sparse arrays, representing colored grid squares. Uncolored grid squares remain undefined.
And two new functions, in the same scope :
function updateGridModel(pos, color) {
var x = (pos.x - 0.5) / 20;
var y = (pos.y - 0.5) / 20;
color = color.substr(1).toLowerCase();
if (!gridModel[x]) {
gridModel[x] = [];
}
gridModel[x][y] = palette.indexOf(color);
}
function paletteTally() {
//initialise an array, same length as palettes, with zeros
var arr = palette.map(function () {
return 0;
});
for (var x = 0; x < gridModel.length; x++) {
if (gridModel[x]) {
for (var y = 0; y < gridModel[x].length; y++) {
if (gridModel[x][y] !== undefined) {
arr[gridModel[x][y]] += 1;
}
}
}
}
return arr;
}
Modify the canvas's click handler to keep the gridModel up to date :
$(c_canvas).click(function (evt) {
var pos = getNearestSquare(getMousePos(c_canvas, evt));
if (pos != null) {
context.fillStyle = currentColor;
context.fillRect(pos.x, pos.y, 19, 19);
incrementValue();
updateGridModel(pos, currentColor); //keep the gridModel up to date.
}
});
Modify printColor() as follows :
function printColor(color) {
currentColor = color.toHexString();
$(".label").text(currentColor);
}
Modify the .spectrum() options and add an initialising call to printColor() as follows :
$("#showPaletteOnly").spectrum({
color: palette[0],
showPaletteOnly: true,
showPalette: true,
hideAfterPaletteSelect: true,
change: printColor,
palette: [palette] //<<<< palette is now defined as an outer var
});
printColor( $("#showPaletteOnly").spectrum('get') );//initialize currentcolor and $(".label").text(...) .
Now paletteTally() will return an array congruent with palette containing counts of each color.
EDIT 1
Original code above was untested but is now debugged and includes improved spectrum options. Demo.