Well i have this SVG canvas element, i've got to the point so far that once a user clicks and drags the canvas is moved about and off-screen elements become on screen etc....
However i have this is issue in which when ever the user then goes and click and drags again then the translate co-ords reset to 0, which makes the canvas jump back to 0,0.
Here is the code that i've Got for those of you whio don't wanna use JS fiddle
Here is the JSfiddle demo - https://jsfiddle.net/2cu2jvbp/2/
edit: Got the solution - here is a JSfiddle DEMO https://jsfiddle.net/hsqnzh5w/
Any and all sugesstion will really help.
var states = '', stateOrigin;
var root = document.getElementById("svgCanvas");
var viewport = root.getElementById("viewport");
var storeCo =[];
function setAttributes(element, attribute)
{
for(var n in attribute) //rool through all attributes that have been created.
{
element.setAttributeNS(null, n, attribute[n]);
}
}
function setupEventHandlers() //self explanatory;
{
setAttributes(root, {
"onmousedown": "mouseDown(evt)", //do function
"onmouseup": "mouseUp(evt)",
"onmousemove": "mouseMove(evt)",
});
}
setupEventHandlers();
function setTranslate(element, x,y,scale) {
var m = "translate(" + x + "," + y+")"+ "scale"+"("+scale+")";
element.setAttribute("transform", m);
}
function getMousePoint(evt) { //this creates an SVG point object with the co-ords of where the mouse has been clicked.
var points = root.createSVGPoint();
points.x = evt.clientX;
points.Y = evt.clientY;
return points;
}
function mouseDown(evt)
{
var value;
if(evt.target == root || viewport)
{
states = "pan";
stateOrigin = getMousePoint(evt);
console.log(value);
}
}
function mouseMove(evt)
{
var pointsLive = getMousePoint(evt);
if(states == "pan")
{
setTranslate(viewport,pointsLive.x - stateOrigin.x, pointsLive.Y - stateOrigin.Y, 1.0); //is this re-intializing every turn?
storeCo[0] = pointsLive.x - stateOrigin.x
storeCo[1] = pointsLive.Y - stateOrigin.Y;
}
else if(states == "store")
{
setTranslate(viewport,storeCo[0],storeCo[1],1); // store the co-ords!!!
stateOrigin = pointsLive; //replaces the old stateOrigin with the new state
states = "stop";
}
}
function mouseUp(evt)
{
if(states == "pan")
{
states = "store";
if(states == "stop")
{
states ='';
}
}
}
In your mousedown function, you are not accounting for the fact that the element might already have a transform and you are just overwriting it.
You are going to need to either look for, and parse, any existing transform. Or an easier approach would be to keep a record of the old x and y offsets and when a new mousedown happens add them to the new offset.
Related
I am using leaflet with leaflet label. Sometimes the markers overlap, which is bad for the UX and therefore I have implemented the following Spiderfier functionality:
/*Geometry*/
//Abstract Shape function capable to check intersection
function Shape(params) {
Initializable.call(this, params);
this.initialize("Type", "Default");
//Let's know whether intersection is symmetric
this.initialize("Symmetric", true);
this.initialize("Intersects", function (shape) {
return false;
});
}
//These rectangles have two horizontal and two vertical sides
function HorizontalVerticalRectangle(params) {
params.Type = "HorizontalVerticalRectangle";
var self = this;
if (typeof params.Intersects !== "function") {
//Default Intersects function
params.Intersects = function (shape) {
//If the two shapes have the same types and self is not to the right, left, bottom or top compared to shape then they intersect each-other
if (shape.Type === self.Type) {
return !((self.TopLeft.x > shape.BottomRight.x) ||
(self.BottomRight.x < shape.TopLeft.x) ||
(self.TopLeft.y > shape.BottomRight.y) ||
(self.BottomRight.y < shape.TopLeft.y));
//In case of top half circles, we need to make sure that the horizontal square collides the circle and in the top half
} else if (shape.Type === "TopHalfCircle") {
return (self.TopLeft.y <= shape.Center.y) && HorizontalVerticalRectangle.prototype.CollidesCircle(self, shape.Center.x, shape.Center.y, shape.Diameter / 2);
}
//Not implemented
return false;
};
}
Shape.call(this, params);
this.initialize("TopLeft", { x: 0, y: 0 });
this.initialize("BottomRight", { x: 0, y: 0 });
//Make sure the x and y coordinates are kept as floats
this.TopLeft.x = parseFloat(this.TopLeft.x);
this.TopLeft.y = parseFloat(this.TopLeft.y);
this.BottomRight.x = parseFloat(this.BottomRight.x);
this.BottomRight.y = parseFloat(this.BottomRight.y);
//Coordinate setters
this.setTopLeftX = function (x) {
self.TopLeft.x = parseFloat(x);
};
this.setTopLeftY = function (y) {
self.TopLeft.y = parseFloat(y);
};
this.setBottomRightX = function (x) {
self.BottomRight.x = parseFloat(x);
};
this.setBottomRightY = function (y) {
self.BottomRight.y = parseFloat(y);
};
}
HorizontalVerticalRectangle.prototype.CollidesCircle = function (horizontalRectangle, centerX, centerY, radius) {
var deltaX = centerX - Math.max(horizontalRectangle.TopLeft.x, Math.min(centerX, horizontalRectangle.BottomRight.x));
var deltaY = centerY - Math.max(horizontalRectangle.TopLeft.y, Math.min(centerY, horizontalRectangle.BottomRight.y));
return Math.pow(deltaX, 2) + Math.pow(deltaY, 2) <= Math.pow(radius, 2);
};
//These are circles where the center has the maximum y and the shape is upwards on screens
function TopHalfCircle(params) {
params.Type = "TopHalfCircle";
var self = this;
if (typeof params.Intersects !== "function") {
//Default Intersects function
params.Intersects = function (shape) {
//If the two shapes have identical type, none of them is above (below in coordinates) the other by more than the other's radius and the full circles intersect,
//then the half circles intersect each-other
if (shape.Type === self.Type) {
return ((self.Center.y - shape.Center.y) < (self.Diameter / 2)) &&
((shape.Center.y - self.Center.y) < (shape.Diameter / 2)) &&
(Math.pow(self.Center.x - shape.Center.x, 2) + Math.pow(self.Center.y - shape.Center.y, 2) < Math.pow(((self.Diameter + shape.Diameter) / 2), 2));
//In case of top horizontal vertical rectangle, we need to make sure that the horizontal square collides the circle and in the top half
} else if (shape.Type === "HorizontalVerticalRectangle") {
return (shape.TopLeft.y <= self.Center.y) && HorizontalVerticalRectangle.prototype.CollidesCircle(shape, self.Center.x, self.Center.y, self.Diameter / 2);
}
//Not Implemented
return false;
};
}
Shape.call(this, params);
this.initialize("Center", { x: 0, y: 0 });
this.initialize("Diameter", 0);
//Make sure the coordinates and diameter are kept as floats
this.Center.x = parseFloat(this.Center.x);
this.Center.y = parseFloat(this.Center.y);
this.Diameter = parseFloat(this.Diameter);
//Setters
this.setCenterX = function (x) {
self.Center.x = parseFloat(x);
};
this.setCenterY = function (y) {
self.Center.y = parseFloat(y);
};
this.setDiameter = function (d) {
self.Diameter = parseFloat(d);
};
}
//Placement strategies for markers, but they can be used for different purposes as well
var PlacementStrategies = {
//This function finds groups of shapes seeing which shape intersects which other shape
Group: function (shapes, comparator) {
if (typeof comparator !== "function") {
comparator = function () {
return true;
};
}
//This variable is empty at start, but at the end will hold the shape groups
var groups = [];
//Traverse the shapes to build the groups
for (var shapeIndex in shapes) {
//This variable will hold false if the shape does not fit into any existing group and the group index otherwise
var foundGroup = false;
//Traverse the groups to find whether a group where the shape fits in already exists
for (var groupIndex = 0; groupIndex < groups.length; groupIndex++) {
//Traverse the shapes of the current group to see whether any of them intersects the shape
for (var innerShapeIndex = 0; (groupIndex < groups.length) && (innerShapeIndex < groups[groupIndex].length) ; innerShapeIndex++) {
//If shape intersects with the current group's current shape, then set foundGroup and exit two for cycles
if (Shape.prototype.intersects(shapes[shapeIndex], shapes[groups[groupIndex][innerShapeIndex]])) {
foundGroup = groupIndex;
innerShapeIndex = groups[groupIndex].length;
groupIndex = groups.length;
}
}
}
//If the shape does not fit into any groups, then we create its own group
if (foundGroup === false) {
groups.push([shapeIndex]);
//Otherwise we search for the location where the shape fits best
} else {
//Desired location. If it results in false, then the shape will be pushed to the end, otherwise it will be inserted at insertIndex
var insertIndex = false;
//Traverse the shapes of the found group to find the desired location to insert
for (var innerShapeIndex = 0; innerShapeIndex < groups[foundGroup].length; innerShapeIndex++) {
//If the shape to be inserted is "smaller" than the found group's current shape, then store the index and quit the cycle
if (!comparator(shapes[groups[foundGroup][innerShapeIndex]], shapes[shapeIndex])) {
insertIndex = innerShapeIndex;
innerShapeIndex = groups[foundGroup].length;
}
}
//Insert the shape into the desired location or to the end if there was no desired middle location
if (insertIndex === false) {
groups[foundGroup].push(shapeIndex);
} else {
groups[foundGroup].splice(insertIndex, 0, shapeIndex);
}
}
}
return groups;
},
//This function merges shape groups if they intersect each-other
MergeGroup: function (shapes, groups, merged, comparator) {
if (typeof comparator !== "function") {
comparator = function () {
return true;
};
}
//This time we merge the contents of the groups into the first index
mergeIssued = true;
while (mergeIssued) {
//There was no merge issued yet
mergeIssued = false;
//Traverse the main groups
for (var mergeIndex in merged) {
//Traverse the groups to merge with
for (var innerMergeIndex in merged[mergeIndex]) {
//If the group to merge with is empty, then it was already parsed
if ((merged[merged[mergeIndex][innerMergeIndex]]) && (merged[merged[mergeIndex][innerMergeIndex]].length > 0)) {
//Traverse the inner groups of the inner group
for (var toMove in merged[merged[mergeIndex][innerMergeIndex]]) {
//Move them if they are not yet present in the main merge group
if (merged[mergeIndex].indexOf(merged[merged[mergeIndex][innerMergeIndex]][toMove]) === -1) {
merged[mergeIndex].push(merged[merged[mergeIndex][innerMergeIndex]][toMove]);
mergeIssued = true;
}
//Remove the content of the inner group to avoid duplicates
merged[merged[mergeIndex][innerMergeIndex]] = [];
}
}
}
}
}
//Traverse the merge groups to move the shapes
for (var mergeIndex in merged) {
//Traverse the inner groups where we read the shapes from
for (var innerMergeIndex in merged[mergeIndex]) {
//Traverse the shapes of the inner group
for (var shapeIndex in groups[merged[mergeIndex][innerMergeIndex]]) {
//If the shape is not yet present in the target group, we move it
if (groups[mergeIndex].indexOf(groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]) === -1) {
//A variable which will hold the index of insertion or false, if the element should be the lasts
var insertLocation = false;
//Traverse the shapes of the target group to find the correct location
for (var targetIndex = 0; (insertLocation === false) && (targetIndex < groups[mergeIndex].length) ; targetIndex++) {
//If the shape located at the current index is not "smaller" than the shape to be inserted, then we found the target location
if (!comparator(shapes[groups[mergeIndex][targetIndex]], shapes[groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]])) {
insertLocation = targetIndex;
}
}
//If there was no "bigger" element, then push at the end of the array
if (insertLocation === false) {
groups[mergeIndex].push(groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]);
//Otherwise insert it to the correct location
} else {
groups[mergeIndex].splice(insertLocation, 0, groups[merged[mergeIndex][innerMergeIndex]][shapeIndex]);
}
}
}
//Clear the group where we moved the shapes from
groups[merged[mergeIndex][innerMergeIndex]] = [];
}
}
//We copy the non-empty groups into another container
var finalGroups = [];
for (var groupIndex in groups) {
if (groups[groupIndex].length > 0) {
finalGroups.push(groups[groupIndex]);
}
}
//And return it
return finalGroups;
},
//This strategy moves rectangles inside a group into a semi circle upwards on the screen
SemiCircleHorizontalRectangles: function (shapes, groups) {
//If groups is falsy, then this is the first try
if (!groups) {
//Which means that we need to create it by calling PlacementStrategies.Group with the comparator desired here
groups = PlacementStrategies.Group(shapes, function (shape1, shape2) {
//The shapes to the left are "smaller" to minimize line collisions
return shape1.TopLeft.x < shape2.TopLeft.x;
});
}
//This will hold top circles of the groups of shapes
var groupTopCircles = [];
//Traverse the raw groups
for (var groupIndex in groups) {
//We need to know the center of the circle, which will be the middle point of the horizontal coordinates and the lowest point in the circle
var maxY = false;
var minX = false;
var maxX = false;
//We need to know the half periphery to calculate the diameter
var halfPeriphery = 0;
//Traverse the shapes in the group
for (var innerShapeIndex in groups[groupIndex]) {
//Calculate the values where we calculate the center coordinates from
if ((minX === false) || (minX > shapes[groups[groupIndex][innerShapeIndex]].TopLeft.x)) {
minX = shapes[groups[groupIndex][innerShapeIndex]].TopLeft.x;
}
if ((maxX === false) || (maxX < shapes[groups[groupIndex][innerShapeIndex]].BottomRight.x)) {
maxX = shapes[groups[groupIndex][innerShapeIndex]].BottomRight.x;
}
if ((maxY === false) || (maxY < shapes[groups[groupIndex][innerShapeIndex]].BottomRight.y)) {
maxY = shapes[groups[groupIndex][innerShapeIndex]].BottomRight.y;
}
//Add the length of the diagonal of the shape to halfPeriphery
halfPeriphery += Math.sqrt(Math.pow(shapes[groups[groupIndex][innerShapeIndex]].BottomRight.x - shapes[groups[groupIndex][innerShapeIndex]].TopLeft.x, 2) + Math.pow(shapes[groups[groupIndex][innerShapeIndex]].BottomRight.y - shapes[groups[groupIndex][innerShapeIndex]].TopLeft.y, 2));
}
//Add the half circle to the container
groupTopCircles[groupIndex] = new TopHalfCircle({ Center: { x: (minX + maxX) / 2, y: maxY }, Diameter: 2 * halfPeriphery / Math.PI });
}
//Container for groups to be merged
var merged;
//Traverse all the shapes
for (var halfCircleIndex = 0; halfCircleIndex < groupTopCircles.length; halfCircleIndex++) {
var s1 = (groups[halfCircleIndex].length === 1) ? shapes[groups[halfCircleIndex][0]] : groupTopCircles[halfCircleIndex];
//Traverse the "later" shapes
for (var secondHalfCircleIndex = halfCircleIndex + 1; secondHalfCircleIndex < groupTopCircles.length; secondHalfCircleIndex++) {
var s2 = (groups[secondHalfCircleIndex].length === 1) ? shapes[groups[secondHalfCircleIndex][0]] : groupTopCircles[secondHalfCircleIndex];
//If the two half circles intersect each-other, then merge them
if (Shape.prototype.intersects(s1, s2)) {
if (!merged) {
merged = {};
}
if (!merged[halfCircleIndex]) {
merged[halfCircleIndex] = [];
}
//We always merge into the first group
merged[halfCircleIndex].push(secondHalfCircleIndex);
}
}
}
//If there was a merge then we do the effective merging and repeat this strategy for the resulting half-circles
if (merged) {
return PlacementStrategies.SemiCircleHorizontalRectangles(shapes, PlacementStrategies.MergeGroup(shapes, groups, merged, function (shape1, shape2) {
//We will order horizontal-verticle rectangles here, we might refactor this function to get a comparator instead later
return shape1.TopLeft.x < shape2.TopLeft.x;
}));
}
//Angle iterator for the half circle
var angle;
//The amount of step with the angle iterator
var angleStep;
//Traverse the groups to change the coordinates
for (var groupIndex in groups) {
//If the group has a single element, then we jump over it
if (groups[groupIndex].length > 1) {
//Initialize the angle iterator and calculate its step size
angle = Math.PI;
angleStep = angle / (groups[groupIndex].length - 1);
//Traverse the shapes
for (var shapeIndex in groups[groupIndex]) {
//The translation is calculated based on circle coordinates
var translation = {
x: groupTopCircles[groupIndex].Center.x + (groupTopCircles[groupIndex].Diameter * Math.cos(angle) / 2),
y: groupTopCircles[groupIndex].Center.y + (groupTopCircles[groupIndex].Diameter * Math.sin(angle) / 2)
};
//The middle of the rectangles will place at the desired point and we need the middle coordinates for that
var halfDiffX = (shapes[groups[groupIndex][shapeIndex]].BottomRight.x - shapes[groups[groupIndex][shapeIndex]].TopLeft.x) / 2;
var halfDiffY = (shapes[groups[groupIndex][shapeIndex]].BottomRight.y - shapes[groups[groupIndex][shapeIndex]].TopLeft.y) / 2;
//Calculate the new bounds of the rectangle and step the iterator
shapes[groups[groupIndex][shapeIndex]].setTopLeftX(translation.x - halfDiffX);
shapes[groups[groupIndex][shapeIndex]].setTopLeftY(translation.y - halfDiffY);
shapes[groups[groupIndex][shapeIndex]].setBottomRightX(translation.x + halfDiffX);
shapes[groups[groupIndex][shapeIndex]].setBottomRightY(translation.y + halfDiffY);
angle += angleStep;
}
}
}
return shapes;
}
};
//General intersects function for shapes, which gets two shapes and checks whether they intersect each-other
Shape.prototype.intersects = function (shape1, shape2) {
//If the first shape is symmetric and the types of shapes match, it is enough to check a single direction of intersection
//Otherwise we need to check both directions
return ((shape1.Symmetric) && (shape1.Type === shape2.Type)) ? (shape1.Intersects(shape2)) : (shape1.Intersects(shape2) || shape2.Intersects(shape1));
};
/*Geometry*/
/*Spiderfier*/
function Spiderfier(params) {
Initializable.call(this, params);
var self = this;
var isSpiderfied = false;
this.defaultFunction = function () { };
//Custom Spiderfy Events
this.initialize("OnSpiderfy", this.defaultFunction, true);
this.initialize("OnUnspiderfy", this.defaultFunction, true);
this.initialize("rows", [], true);
this.initialize("cm", function () {
return cachedMarkers;
}, true);
this.initialize("options", {});
this.SpiderLines = {};
this.isCurrentlySpiderfied = function () {
return isSpiderfied;
};
this.refreshRows = function (r, stopRefresh) {
rows = r;
if (isSpiderfied && (!stopRefresh)) {
self.spiderfy();
}
};
this.spiderfy = function (r) {
if (r) {
self.refreshRows(r, true);
}
params.OnSpiderfy(rows, self);
isSpiderfied = true;
};
this.unspiderfy = function (r) {
if (r) {
self.refreshRows(r, true);
}
params.OnUnspiderfy(rows, self);
isSpiderfied = false;
};
//Handles marker draw and spiderfying
this.drawAndSpiderfy = function (r, o) {
//First handle the spiderfy thing
if (o) {
self.options = o;
}
if (self.isCurrentlySpiderfied()) {
self.spiderfy(r, params.cm());
drawSpiderMarkers(r, params.cm(), self);
} else {
self.unspiderfy(r, params.cm());
}
//And then draw the markers
drawMarkers(rows, options);
};
}
//Gets the rectangles of the markers
function markersToRectangles(rows) {
var shapes = [];
var lowPoint;
for (var rowIndex in rows) {
//Convert the geographical point of the marker into graphical point
lowPoint = map.latLngToLayerPoint(L.latLng(rows[rowIndex].RealLat, rows[rowIndex].RealLon));
shapes.push(new HorizontalVerticalRectangle({
TopLeft: { x: lowPoint.x - 18, y: lowPoint.y - 44 },
BottomRight: { x: lowPoint.x + 18 + 0, y: lowPoint.y }
}));
}
return shapes;
}
//Spiderfies rectangles with half circle strategy
function RectangleHalfCircleSpiderfy(rows, spdfr) {
//Initialize real latitude and longitude if not already done so
for (var rowIndex in rows) {
if (!rows[rowIndex].RealLat) {
rows[rowIndex].RealLat = rows[rowIndex].Lat;
rows[rowIndex].RealLon = rows[rowIndex].Lon;
}
}
//Gather the desired rectangles
var rectangles = PlacementStrategies.SemiCircleHorizontalRectangles(markersToRectangles(rows));
//Store the geographic coordinates
for (var rowIndex in rectangles) {
//Convert graphical coordinates into geographic coordinates
var location = map.layerPointToLatLng(L.point(rectangles[rowIndex].TopLeft.x + 14, rectangles[rowIndex].BottomRight.y));
rows[rowIndex].Lat = location.lat;
rows[rowIndex].Lon = location.lng;
}
}
function normalUnspiderfy(rows, spiderfier) {
for (var rowIndex in rows) {
if (rows[rowIndex].RealLat !== undefined) {
rows[rowIndex].Lat = rows[rowIndex].RealLat;
rows[rowIndex].Lon = rows[rowIndex].RealLon;
delete rows[rowIndex].RealLat;
delete rows[rowIndex].RealLon;
}
}
for (var lineIndex in spiderfier.SpiderLines) {
map.removeLayer(spiderfier.SpiderLines[lineIndex].polyLine);
}
spiderfier.SpiderLines = {};
}
//Draws spider markers
function drawSpiderMarkers(rows, cachedMarkers, spiderfier) {
//For each row...
for (var i = 0; i < rows.length; i++) {
//If real location exists and differs from the display location and there is either no spider line yet or points to a different location than the expected one
if (rows[i].RealLat && rows[i].RealLon &&
((rows[i].Lat != rows[i].RealLat) || (rows[i].Lon != rows[i].RealLon)) &&
((!spiderfier.SpiderLines[i]) || (spiderfier.SpiderLines[i].location.Lat != rows[i].Lat) || (spiderfier.SpiderLines[i].location.Lon != rows[i].Lon))
) {
//Then check whether the spider line exists and remove it if so
if (spiderfier.SpiderLines[i]) {
map.removeLayer(spiderfier.SpiderLines[i].polyLine);
}
//And generate a new spider line
spiderfier.SpiderLines[i] = { location: new L.LatLng(rows[i].Lat, rows[i].Lon), realLocation: new L.LatLng(rows[i].RealLat, rows[i].RealLon) };
spiderfier.SpiderLines[i].polyLine = L.polyline([spiderfier.SpiderLines[i].location, spiderfier.SpiderLines[i].realLocation]);
spiderfier.SpiderLines[i].polyLine.options.weight = 2;
spiderfier.SpiderLines[i].polyLine.options.color = "#5f0df1";
spiderfier.SpiderLines[i].polyLine.addTo(map);
}
}
}
var spiderfier;
/*Spiderfier*/
function getStrategyName(code) {
switch (code) {
case 2: return "Grouped";
case 3: return "RectangleHalfCircleSpiderfy";
default: return "Unspecified";
}
}
function drawStrategicMarkers(rows, drawOpt) {
if (drawOpt.strategy < 3) {
if (drawOpt.strategy === 2) {
drawOpt.grouped = true;
}
return drawMarkers(rows, drawOpt);
} else {
if (!spiderfier) {
window["spiderfier"] = new Spiderfier({
OnSpiderfy: window[getStrategyName(drawOpt.strategy)],
OnUnspiderfy: normalUnspiderfy,
});
}
spiderfier.drawAndSpiderfy(rows);
}
}
Explanation: This calculates the graphical coordinates of the marker's rectangle and finds out which rectangles belong to a group. A group will be a top half circle where markers are displayed at the periphery and when we have such half-circles, they are checked against each-other, so if they intersect each-other, then they are merged into a new group. If a group contains a single marker, then its rectangle is taken into consideration rather than its top half circle. At the end markers are translated to their desired location on their group (top half circle periphery).
This works perfectly, however, the problem is that this takes into account only the markers' rectangles and does not take into account label sizes at all (a label is displayed to the right of the corresponding marker and the two together should be considered as a single rectangle). The reason for this is very simple: I can gather label sizes, but only after everything was drawn. My question is as follows: If I know what the label will contain, is there a reliable formula with which I can gather the bounds and limits of the label container, so that I could check for it to not overlap with other markers or labels as well?
After labels are generated, this extremely hacky way is how I could gather information about the labels' size:
function getLabelSize(index) {
var labelContext = $(".leaflet-map-pane .leaflet-label:eq(" + index + ")");
return {width: labelContext.width(), height: labelContext.height()};
}
To draw the markers, measure the labels and then redraw the markers just to get the label sizes this way is so hacky that I rather allow labels to intersect markers or other labels, which is a sad decision. Therefore I wonder: is there a way to get the width and height of a label which was not yet drawn based on its future content?
The content looks like this:
<div class="leaflet-label leaflet-zoom-animated leaflet-label-right" style="z-index: 540; transform: translate3d(912px, 500px, 0px); opacity: 1;">
<p class="orange">34534</p>
<p>3343453</p>
</div>
Of course, that div has padding and border, but I will be able to add the needed values if I am able to read the inner width and height somehow.
If I know what the label will contain, is there a reliable formula with which I can gather the bounds and limits of the label container?
No.
In HTML, you cannot know the computed dimensions of a block element before that block element gets added to the DOM. This is due to a variety of reasons; mainly the fact that there might be lots of different (non-explicit) CSS selectors that might apply to that block element when it gets added. Plus user-agent stylesheets, GPU font rendering, DPI font size, etc etc etc.
I researched this when working on Leaflet.LayerGroup.Collision.
this extremely hacky way is how I could gather information about the labels' size
Please don't. Use window.getComputedStyle after the element has been added to the DOM.
You are free to remove that element from the DOM (or to remove the Leaflet layer, as it will have the same effect) within the same render frame. You can add an element to the DOM, get its computed dimensions, remove that element to the DOM, and do it so fast that the browser does not hit a frame render in between (among other things, because the browser UI thread is blocked while doing so).
This is how Leaflet.LayerGroup.Collision works: Add everything to the DOM (add all the leaflet layers to the map), get the computed style for everything, add the bounding boxes to the rbush structure, compute collisions, remove elements from the DOM (layers from the map) within one frame.
These are my references created in pixi.js here:
http://brekalo.info/en/reference
If we go to references it loads pixiJS and everything works fine on first load! Then, if we go to another page let's say: http://brekalo.info/en/contact, and the go back to references again - now my references have accelerated text movement and rotation and it keeps accelerate on each reference page load!
Here is my javascript/pixi code below:
function initiatePixi() {
Object.keys(PIXI.utils.TextureCache).forEach(function(texture) {
PIXI.utils.TextureCache[texture].destroy(true);}
);
// create an new instance of a pixi stage
var container = new PIXI.Container();
// create a renderer instance.
renderer = PIXI.autoDetectRenderer(frameWidth, frameHeight, transparent = false, antialias = true);
// set renderer frame background color
renderer.backgroundColor = 0xFFFFFF;
// add the renderer view element to the DOM
document.getElementById('pixi-frame').appendChild(renderer.view);
// create references
createReferences(animate); // callback to animate frame
function createReferences(callback) {
// Create text container
textContainer = new PIXI.Container();
textContainer.x = 0;
textContainer.y = 0;
for (i = 0; i < references.length; i++) {
var style = {
font:"22px Verdana",
fill:getRandomColor()
};
var text = new PIXI.Text(references[i], style);
text.x = getRandomInteger(20, 440); // text position x
text.y = getRandomInteger(20, 440); // text position y
text.anchor.set(0.5, 0.5); // set text anchor point to the center of text
text.rotation = getRandomInteger(0, rotationLockDeg) * 0.0174532925; // set text rotation
// make the text interactive
text.interactive = true;
// create urls on text click
text.on("click", function (e) {
var win = window.open("http://" + this.text, '_blank');
win.focus();
});
textContainer.addChild(text);
rotateText(); // rotate text each second
}
container.addChild(textContainer);
// callback
if (callback && typeof(callback) === "function") {
callback();
}
}
function animate() {
requestAnimationFrame(animate);
// render the stage
renderer.render(container);
}
function rotateText() {
var rotateTimer = setInterval(function () {
for (var key in textContainer.children) { // loop each text object
var text = textContainer.children[key];
if(text.rotation / 0.0174532925 < -rotationLockDeg || text.rotation / 0.0174532925 > rotationLockDeg) {
if(text.rotation / 0.0174532925 < -rotationLockDeg)
text.rotation = -rotationLockRad;
if(text.rotation / 0.0174532925 > rotationLockDeg)
text.rotation = rotationLockRad;
rotation = -rotation;
}
text.rotation += rotation; // rotate text by rotate speed in degree
if(text.x < 0 || text.x > 460)
dx = -dx;
if(text.y < 0 || text.y > 460)
dy = -dy;
text.x += dx;
text.y += dy;
}
}, 75);
}
// get random integer between given range (eg 1-10)
function getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// random hex color generator
function getRandomColor() {
var letters = '0123456789ABCDEF'.split('');
var color = '#';
for (var i = 0; i < 6; i++ ) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
Thanks in advance!
:: cheers ::
Josip
To expand #Cristy's comment to an answer:
The answer lies in the same reason as why your question title is wrong: There is indeed NO page refresh when doing what you describe. If there were, you wouldn't have that problem in the first place. Try it out, hit F5 a few times on you animated page, it will stay the same speed.
The reason is that you are running a angular based single page application, and only exchange the loaded view content on a route change. This does not stop your already running animation code from continuing to run in the background while you navigate to another view, so that when you return to the animated tab you will create another set of interval timers for your animation, which will result in more executions and thus a visually faster animation.
#Cristy thanks for the advice!
Here is how I manage to solve this..
I put one property in my pixi-parameters.js:
pixiWasLoaded = false;
Then, when I call initiatePixi() function, I set:
pixiWasLoaded = true;
Now in my controllers.js I have this piece of code:
.run( function($rootScope, $location, $window) {
$rootScope.$watch(function() {
return $location.path();
},
function(page){
if(page == "/hr/reference" || page == "/en/references"){
if($window.pixiWasLoaded)
$window.addRendererElementToDOM();
else
loadReferences();
}
});
});
It checks if references page is loaded and then uses $window to find my global variable "pixiWasLoaded" and if it's not loaded then it loads PixiJS using loadReferences() function.. and if is already loaded it calls my part of code to add render-view to DOM so my animate function can render it..
:: cheers ::
Josip
I'm making a sidescroller using Phaser (latest version) and I want the player's projectiles to go towards the pointer when the player clicks, like they do in this example http://phaser.io/examples/v2/games/tanks. I've used some of the code from the example but in my game, the activePointer x and y co-ordinates seem to only initialise when the game starts and never change. So when the player shoots, it's always going towards the same co-ordinates.
I have the following code (note I have removed bits about item collection, enemies etc. for posting on here):
var SideScroller = SideScroller || {};
var startPosX = 100;
var startPosY = 300;
var shooter;
var playerBullets;
var nextFire = 0;
var fireRate = 100;
var cursors;
var currentLoc;
SideScroller.Game = function () {};
SideScroller.Game.prototype = {
create: function () {
//create player
//params = (game, startPositionX,startPositionY, key, frame)
this.player = this.game.add.sprite(startPosX, startPosY, 'player');
//get canvas width and height for later use
canvasWidth = this.game.canvas.width;
canvasHeight = this.game.canvas.height;
//create enemy
var x = this.game.rnd.between(80, this.game.world.width);
var y = this.game.rnd.between(0, 113);
// Point to shoot projectiles from
// allows rotation, if this had been done on the player object, the graphic would have rotated, which we don't want
this.shooter = this.game.add.sprite(startPosX, startPosY, 'blank');
this.shooter.anchor.setTo(0.5, 0.5);
//make a group of player projectiles
playerBullets = this.game.add.group();
playerBullets.enableBody = true;
playerBullets.physicsBodyType = Phaser.Physics.ARCADE;
playerBullets.createMultiple(1000, 'peePower');
playerBullets.setAll('anchor.x', 0.5);
playerBullets.setAll('anchor.y', 0.5);
playerBullets.setAll('outOfBoundsKill', true);
playerBullets.setAll('checkWorldBounds', true);
//enable physics on the player
this.game.physics.arcade.enable(this.player);
//bring player shooting point to the top (not totally necessary)
this.shooter.bringToTop();
//player gravity
this.player.body.gravity.y = gravity;
//player collides with all four edges of the game world
this.player.body.collideWorldBounds = true;
this.player.anchor.setTo(0.5, 0.5);
//the camera will follow the player in the world
this.game.camera.follow(this.player);
//move player with cursor keys
cursors = this.game.input.keyboard.createCursorKeys();
},
update: function () {
currentLoc = this.game.input.activePointer;
//collision between player and platforms
this.game.physics.arcade.collide(this.player, this.blockedLayer, null, null, this);
//make co-ordinates match
this.shooter.x = this.player.x;
this.shooter.y = this.player.y;
//this.shooter's angle towards
this.shooter.rotation = this.game.physics.arcade.angleToPointer(this.shooter, this.game.input.activePointer);
//only respond to keys if the player is alive
if (this.player.alive) {
this.player.body.velocity.x = 0;
if (this.game.input.activePointer.isDown) {
console.log("pointer is down");
this.fire();
}
else if (cursors.right.isDown) {
this.playerForward();
}
else if (cursors.left.isDown) {
this.playerBack();
}
else if (cursors.up.isDown) {
this.playerJump();
}
else if (cursors.down.isDown) {
this.fire();
this.playerDuck();
}
}
},
fire: function () {
//for debugging
console.log("fire was called");
console.log(this.game.input.activePointer.x);
console.log(this.game.input.activePointer.y);
if (this.game.time.now > nextFire && playerBullets.countDead() > 0)
{
nextFire = this.game.time.now + fireRate;
var bullet = playerBullets.getFirstExists(false);
bullet.reset(this.shooter.x, this.shooter.y);
currentLoc = this.game.input.activePointer;
bullet.rotation = this.game.physics.arcade.moveToPointer(bullet, 1000, currentLoc, 1000);
console.log(this.game.input.activePointer);
}
},
playerForward: function () {
this.player.loadTexture('player');
this.player.body.setSize(this.player.standDimensions.width, this.player.standDimensions.height);
this.player.body.velocity.x = 700;
this.player.isMoving = true;
//console.log("Forward height:" + this.player.standDimensions.height);
//console.log("Forward width:" + this.player.standDimensions.width);
},
playerBack: function () {
this.player.loadTexture('playerBack');
this.player.body.velocity.x -= 700;
this.player.isMoving = true;
},
playerJump: function () {
if (this.player.body.blocked.down) {
this.player.body.velocity.y -= 700;
this.player.loadTexture('playerJump');
//console.log("Jump height:" + this.player.jumpDimensions.height);
//console.log("Jump width:" + this.player.jumpDimensions.width);
}
},
playerDuck: function () {
//change image and update the body size for the physics engine
this.player.loadTexture('playerDuck');
this.player.body.setSize(this.player.duckedDimensions.width, this.player.duckedDimensions.height);
//keep track of whether player is ducked or not
this.player.isDucked = true;
},
playerDead: function () {
//set to dead (this doesn't affect rendering)
this.player.alive = false;
//stop moving to the right
this.player.body.velocity.x = 0;
//change sprite image
this.player.loadTexture('playerDead');
},
};
Shooter is a blank sprite on top of the player (much like the turret in the tank example) to allow for rotation without the player rotating (please let me know also if there's a better way to do that!).
I tried updating the currentLoc variable in the update method to the activePointer location but that didn't work.
In addition, this condition has never been hit:
if (this.game.input.activePointer.isDown) {
console.log("pointer is down");
this.fire();
}
So something must be going awry with detecting mouse clicks and I don't know if that's part of the problem?
I think you should look it up in the API. There are few points in your code that are questionable.
http://phaser.io/docs/2.3.0/Phaser.Pointer.html
http://phaser.io/docs/2.3.0/Phaser.Physics.Arcade.html#moveToPointer
The point is that you are actually giving the reference to the pointer (to currentLoc) but not the position. So it should always fire to 0;0.
And for the isDown detection, have you done it in the update function or somewhere else?
Hope i could help!
I am trying to re-size a circle using papeJS but since i used two onMouseDrag function it if conflicting. I am unable to create it. Can anyone help me. Here is the fiddle with circle
Here is the code.
<script type="text/paperscript" canvas="canvas">
var raster = new Raster({
source: 'Chrysanthemum.jpg',
position: view.center
});
var path = null;
var circles = [];
var isDrawing = false;
var draggingIndex = -1;
var segment, movePath;
var resize = false;
project.activeLayer.selected = false;
function onMouseDrag(event) {
if (!isDrawing && circles.length > 0) {
for (var ix = 0; ix < circles.length; ix++) {
if (circles[ix].contains(event.point)) {
draggingIndex = ix;
break;
}
}
}
if (draggingIndex > -1) {
circles[draggingIndex].position = event.point;
} else {
path = new Path.Circle({
center: event.point,
radius: (event.downPoint - event.point).length,
fillColor: null,
strokeColor: 'black',
strokeWidth: 10
});
path.removeOnDrag();
isDrawing = true;
}
}
;
function onMouseUp(event) {
if (isDrawing) {
circles.push(path);
}
isDrawing = false;
draggingIndex = -1;
}
;
function onMouseMove(event) {
project.activeLayer.selected = false;
if (event.item)
event.item.selected = true;
resize = true;
}
var segment, path;
var movePath = false;
function onMouseDown(event) {
segment = path = null;
var hitResult = project.hitTest(event.point, hitOptions);
if (!hitResult)
return;
if (hitResult) {
path = hitResult.item;
if (hitResult.type == 'segment') {
segment = hitResult.segment;
} else if (hitResult.type == 'stroke') {
var location = hitResult.location;
segment = path.insert(location.index + 1, event.point);
path.smooth();
}
}
movePath = hitResult.type == 'fill';
if (movePath)
project.activeLayer.addChild(hitResult.item);
}
</script>
First, your code (on jsfiddle) does not run.
The paperjs external resource returned a 404. https://raw.github.com/paperjs/paper.js/master/dist/paper.js works for paperjs.
The raster source was for a local file, not a URI.
In onMouseDown, project.hitTest references an undefined hitOptions.
It seems from your question that you want to be able to drag the circle segments to resize the circle, and you tried using two onMouseDrag functions to do that, which would not work. Instead, both operations should be in the same onMouseDrag, using if-then-else to choose between them. To make this work as expected, the item that was hit should be stored in onMouseDown instead of whatever circle your code finds at the beginning of onMouseDrag. For example, here onMouseDrag can either "move" or "resize" (jsfiddle here):
<script type="text/paperscript" canvas="myCanvas">
var raster = new Raster({
source: 'http://i140.photobucket.com/albums/r10/Array39/Chrysanthemum.jpg',
position: view.center
});
var circles = [];
var hitItem = null;
var currentAction = null;
function onMouseMove(event) {
project.activeLayer.selected = false;
if (event.item) {
event.item.selected = true;
}
}
function onMouseDown(event) {
hitItem = null;
var aColor = new Color('black');
for (var i = 0; i < circles.length; i++) {
circles[i].fillColor = aColor;
}
view.draw();
var hitResult = project.hitTest(event.point);
for (var i = 0; i < circles.length; i++) {
circles[i].fillColor = null;
}
view.draw();
if (!hitResult) {
return; //only happens if we don't even hit the raster
}
hitItem = hitResult.item;
if (circles.indexOf(hitItem) < 0) {
var newCircle = new Path.Circle({
center: event.point,
radius: 2,
strokeColor: 'black',
strokeWidth: 10
});
hitItem = newCircle;
circles.push(hitItem);
currentAction = 'resize';
return;
}
if (hitResult.type == 'segment') {
currentAction = 'resize';
} else if (hitResult.type == 'stroke') {
hitItem.insert(hitResult.location.index + 1, event.point);
hitItem.smooth();
currentAction = 'resize';
} else if (hitResult.type == 'fill') {
currentAction = 'move';
}
}
function onMouseDrag(event) {
if (!hitItem) {
return;
}
if (currentAction == 'move') {
hitItem.position = event.point;
} else if (currentAction == 'resize') {
if ((event.downPoint - event.point).length >= 1) {
hitItem.fitBounds(new Rectangle(event.downPoint, event.point), true);
}
}
};
</script>
<canvas id="myCanvas"></canvas>
Also note:
In onMouseDown, the function returns if !hitResult, so you do not need to test if (hitResult) right after that return.
Naming variables the same as objects makes searching more difficult, e.g., in your code path is an instance of Path.
Using the same variable for different purposes makes code more difficult to parse, e.g., in your code path is used to create new circles as well as to store which circle has been selected.
You have multiple variables defined twice: path, movePath, and segment.
If a variable will only be used in a single function, e.g., movePath and segment, then it makes the code more readable if the variable is defined in that function. Also, movePath is only used in a single if-statement, which just adds items back to the layer, but the only items not in the layer have been removed when the circle was originally drawn. Since those items cannot be hit, the item that was hit must already be in the layer.
The variable segment is not used.
It makes the code flow/read better if the functions are ordered logically. In this case, onMouseMove should go first because it happens before the button is clicked. Then onMouseDown goes next because it must happen before the other actions. Then onMouseDrag, and finally onMouseUp.
Instead of creating new circles in onMouseDrag and then throwing them away on the next drag, it makes more sense to create one in onMouseDown if there was no item hit, or if the hit item is not a circle. Then in onMouseDown, you just resize that circle. Path.scale or Path.fitBounds can be used for such resizing.
Instead of using multiple boolean variables to keep track of the current action (e.g., resize vs move), it is more logical to have a single variable keeping track of the current action.
Instead of your code to find whether the point is within a circle, the code I am using temporarily sets the circles' fillColor, do the hitTest, and then clears the circles' fillColor. I did this because when you hit a stroke, the shape of the circle changes, for which your code to find the draggingIndex does not account.
I want to move a specific node (say i have the node reference), to a desired location (say to the top left corner of the canvas). I tried doing something similar to one in the sample project, where they move nodes while dragging with the mouse. But it doesn't seem to work. I am not seeing the node move as i expected. This is the code i have.
$("#someElement").click(function() {
sys.eachNode(function(node, pt) {
if (node.name === "specificNode") {
// moveToOrigin
var s = arbor.Point(1, 1);
var p = sys.fromScreen(s);
node.fixed = true;
node.p = p;
node.fixed = false;
node.tempMass = 1000;
}
}
});
To move a node to a desired position get the desired position relative to the canvas and set it via particle system's fromScreen(...) function:
var point = point;
var pos = canvas.offset();
var s = arbor.Point(point.x-pos.left, point.x-pos.top);
node.p = particleSystem.fromScreen(s);
You need to set position of node first, then again iterate each loop for nodes.
redraw: function () {
gfx.clear()
particleSystem.eachNode(function (node, pt) {
//var node = particleSystem.getNode("Carrol Wahi")
if (node.data.color == "yellow") {
var pos = $(canvas).offset();
var point = particleSystem.fromScreen(arbor.Point(pos.left + 150, pos.top));
node._fixed = true;
node._p = point;
//console.log("x=" + point.x + ", y=" + point.y);
node.tempMass = .1
}
});
particleSystem.eachEdge(function (edge, pt1, pt2) {
// your code goes here
}
particleSystem.eachNode(function (node, pt) {
//your code goes here
})
}