Leaflet.js wrap LatLng positions of line to nearest line - javascript

I want to have the user click two positions and then draw a line between them. This is totally functional and easy. And it also works if the click is outside the -180;180 range.
createPolyLine(outside2, userLocation);
//draw polyline
function createPolyLine(loc1, loc2) {
var latlongs = [loc1, loc2];
console.log(loc1);
var polyline = new L.Polyline(latlongs, {
color: 'white',
opacity: 1,
weight: 1,
clickable: false
}).addTo(map);
}
Now If a user clicks say on longitude of -600 I want to have it automatically wrap into the -180;180 area but only if its the closest path. If its closer to keep the click in the -360:-180 area then it should wrap into the -360;-180 area. Same for the other side in positive direction ofcourse.
Example image of what i mean:
Example of when its closer to use -360;-180 region and not wrap it into -180;180
Example of when its closer to use -180;180 and it would be wrong now
Based on second example but correctly wrapped now
What would be the most efficient way to achieve this auto wrap into either -360;-180 / -180;180 / 180;360 depending on where the closest line between two points would be?

Thanks to the response of Corey Alix I came up with a solution which feels kinda dirty but it works for all the test cases I tried against. In my scenario one point is always in -180;180 range.
// a is always in -180;180
function getPositions(a, b) {
b.lng %= 360;
let abs = Math.abs(a.lng - b.lng);
let absWrap = Math.abs(a.lng - b.wrap().lng);
if(absWrap < abs) {
b = b.wrap();
abs = absWrap;
}
let left = new L.LatLng(b.lat, b.lng - 360);
let right = new L.LatLng(b.lat, b.lng + 360);
let leftAbs = Math.abs(a.lng - left.lng);
if(leftAbs < abs) {
b = left;
abs = leftAbs;
}
if(Math.abs(a.lng - right.lng) < abs) {
b = right;
}
return [a, b];
}
//draw polyline
function createPolyLine(loc1, loc2) {
var latlongs = getPositions(loc2, loc1);
polyline = new L.Polyline(latlongs, {
color: 'white',
opacity: 1,
weight: 1,
clickable: false
}).addTo(map);
}

Related

Can I use an image (.svg) or svg path(?) in Konva.Group()'s clipFunc?

Suppose I have a map of the world.
And I'd like each continent to be an area where I could attach shapes to and drag/reshape them, while always being clipped by the continent's shape borders/limits.
Here's what I have so far:
const stage = new Konva.Stage({
container: 'stage',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.arc(250, 120, 50, 0, Math.PI * 2, false);
ctx.arc(150, 120, 60, 0, Math.PI * 2, false);
},
});
const shape = new Konva.Rect({
x: 150,
y: 70,
width: 100,
height: 50,
fill: "green",
stroke: "black",
strokeWidth: 4,
draggable: true,
});
group.add(shape);
layer.add(group);
stage.add(layer);
body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/8.4.0/konva.min.js"></script>
<div id="stage"></div>
My question is, how could I use the clipFunc to draw a continent's limits? Could I use and image? svg path? I can't seem to find the answer in the docs.
[Edit: Added a new option 2, added demos for options 2 & 3 codepen + snippets.]
TLDR: Nothing totally automatic but two possible options.
Just to confirm - based on
And I'd like each continent to be an area where I could attach shapes
to and drag/reshape them, while always being clipped by the
continent's shape borders/limits.
I think you are asking how to limit the boundaries for dragging a shape to an 'arbitrary' region. I say arbitrary because it is a non-geometric region (not a square, circle, pentagon etc).
It would be fabulous to have a baked-in function to achieve this but sadly I am not aware that it is possible. Here's why:
Dragbounds limits: In terms of what you get 'out of the box', how Konva handles constraining drag position is via the node.dragBoundFunc(). Here is the example from the Konva docs which is straightforward.
// get drag bound function
var dragBoundFunc = node.dragBoundFunc();
// create vertical drag and drop
node.dragBoundFunc(function(pos){
// important pos - is absolute position of the node
// you should return absolute position too
return {
x: this.absolutePosition().x,
y: pos.y
};
});
The gist of this is that we are able to use the code in the dragBoundFunc function to decide if we like the position the shape is being dragged to, or not. If not we can override that 'next' position with our own.
Ok - so that is how dragging is constrained via dragBoundFunc. You can also use the node.on('dragmove') to achieve the same effect - the code would be very similar.
Hit testing
To decide in the dragBoundFunc whether to accept the proposed position of the shape being dragged, we need to carry out 'hit testing'.
[Aside: An important consideration is that, to make a pleasing UI, we should be hit testing at the boundary of the shape that is being dragged - not where the mouse pointer or finger are positioned. Example - think of a circle being dragged with the mouse pointer at its center - we want to show the user the 'hit' UI when the perimeter of the circle goes 'out of bounds' from the perspective of the dragBoundFunc, not when the center hits that point. What this means in effect is that our logic should check the perimeter of the shape for collision with the boundary - that might be simple or more difficult, depending on the shape.]
So we know we want to hit test our dragging shape against an arbitrary, enclosing boundary (the country border).
Option #1: Konva built-in method.
[Update] On developing the demo for this option I discovered that its mainstay, getIntersection(pt), is deliberately disabled (will always return null) when used in a dragmove situation. This is by design and done for performance because the overhead for the process is so costly.
What getIntersection does is to look at a given pixel, from topmost shape down, of the shapes that might overlap the given x, y point. It stops at the first hit. To do this is draws in an off-screen canvas each shape, checks the pixel, and repeats until no shapes remain. As you can tell, quite a costly process to run in-between mousemove steps.
The proposal for this option was to check a bunch of static border points on the stage via getIntersection - if the dragging shape came up as the hit then we would know the border was being crossed.
What point do we give it to check ? So here's the rub - you would have to predefine points on your map that were on the borders. How many points? Enough so that your draggable shapes can't stray very far over the mesh of border points without the hit-test firing. Do it correctly and this would be a very efficient method of hit testing. And it's not as if the borders will be changing regularly.
I made a simple point creator here. This is the view after I created the points around Wombania.
** Option #2: The concept is to create an off-screen canvas the same size as the client rect of the shape being dragged and create a clone of the drag shape therein. Space around the shape would be transparent, shape itself would be colored. We now use a set of pre-defined points along the country boundary, example above. We filter the points inside the shape's clientRect - so we only have a handful to test. We then 'translate' those points to the appropriate location in the off-screen hit canvas and check the color of the pixels at those points - any color whatsoever indicates the dragging shape is 'over' the point we are testing. Any hit means we can break out of the loop and report a boundary collision.
This is optimised in the following ways:
1 - we only make the offscreen canvas once at the dragstart.
2 - we only test the minimum number of boundary points - only those falling in the bounding box of the dragging shape.
Demo here at codepen. Snippet below - best consumed full screen.
const scale = 1,
stage = new Konva.Stage({
container: "container",
width: 500,
height: 400,
draggable: false
}),
layer = new Konva.Layer({
draggable: false
}),
imageShape = new Konva.Image({
x: 0,
y: 0,
draggable: false
}),
// Rect drawn to show client rect of dragging shape
theShapeRect = new Konva.Rect({
stroke: "silver",
strokeWidth: 1,
listening: false
}),
// small dots to show check points
pointCircle = new Konva.Circle({
radius: 30,
fill: "silver",
draggable: false
}),
// the three draggable shape defs - select by button
dragShapes = {
circle: new Konva.Circle({
radius: 30,
fill: "lime",
draggable: true,
visible: false
}),
rectangle: new Konva.Rect({
width: 60,
height: 60,
fill: "lime",
draggable: true,
visible: false
}),
star: new Konva.Star({
numPoints: 6,
innerRadius: 40,
outerRadius: 70,
fill: "lime",
draggable: true,
visible: false
})
},
// data for the check points.
data = `{"pt0":{"x":85.5,"y":44.5},"pt1":{"x":76,"y":62},"pt2":{"x":60,"y":78},"pt3":{"x":47,"y":94},"pt4":{"x":33,"y":115},"pt5":{"x":26,"y":133},"pt6":{"x":17,"y":149},"pt7":{"x":27,"y":171},"pt8":{"x":45,"y":186},"pt9":{"x":69,"y":187},"pt10":{"x":87,"y":191},"pt11":{"x":104,"y":194},"pt12":{"x":123,"y":214},"pt13":{"x":124,"y":238},"pt14":{"x":120,"y":260},"pt15":{"x":94,"y":265},"pt16":{"x":92,"y":275},"pt17":{"x":113,"y":281},"pt18":{"x":130,"y":280},"pt19":{"x":148,"y":280},"pt20":{"x":156,"y":261},"pt21":{"x":169,"y":248},"pt22":{"x":188,"y":251},"pt23":{"x":201,"y":263},"pt24":{"x":207,"y":274},"pt25":{"x":195,"y":281},"pt26":{"x":181,"y":285},"pt27":{"x":183,"y":291},"pt28":{"x":194,"y":293},"pt29":{"x":222,"y":293},"pt30":{"x":242,"y":284},"pt31":{"x":245,"y":257},"pt32":{"x":247,"y":238},"pt33":{"x":263,"y":236},"pt34":{"x":278,"y":240},"pt35":{"x":293,"y":239},"pt36":{"x":305,"y":238},"pt37":{"x":315,"y":237},"pt38":{"x":333,"y":236},"pt39":{"x":337,"y":248},"pt40":{"x":324,"y":258},"pt41":{"x":303,"y":263},"pt42":{"x":314,"y":267},"pt43":{"x":326,"y":273},"pt44":{"x":347,"y":273},"pt45":{"x":364,"y":273},"pt46":{"x":378,"y":260},"pt47":{"x":401,"y":263},"pt48":{"x":422,"y":272},"pt49":{"x":429,"y":278},"pt50":{"x":414,"y":281},"pt51":{"x":400,"y":287},"pt52":{"x":411,"y":294},"pt53":{"x":434,"y":292},"pt54":{"x":462,"y":287},"pt55":{"x":478,"y":275},"pt56":{"x":474,"y":259},"pt57":{"x":466,"y":233},"pt58":{"x":470,"y":208},"pt59":{"x":483,"y":189},"pt60":{"x":484,"y":169},"pt61":{"x":494,"y":153},"pt62":{"x":496,"y":129},"pt63":{"x":489,"y":106},"pt64":{"x":472,"y":91},"pt65":{"x":458,"y":78},"pt66":{"x":443,"y":65},"pt67":{"x":428,"y":54},"pt68":{"x":412,"y":41},"pt69":{"x":394,"y":31},"pt70":{"x":369,"y":23},"pt71":{"x":346,"y":22},"pt72":{"x":323,"y":22},"pt73":{"x":300,"y":23},"pt74":{"x":278,"y":24},"pt75":{"x":265,"y":26},"pt76":{"x":251,"y":30},"pt77":{"x":235,"y":32},"pt78":{"x":220,"y":38},"pt79":{"x":203,"y":44},"pt80":{"x":189,"y":53},"pt81":{"x":174,"y":57},"pt82":{"x":163,"y":51},"pt83":{"x":148,"y":53},"pt84":{"x":128,"y":52},"pt85":{"x":100,"y":51}}`,
// load the data into an object.
pointsList = JSON.parse(data);
// shape is set when the shape-type button is clicked.
let theShape = undefined;
// Add shapes to the layer and layer to stage
layer.add(
imageShape,
dragShapes.circle, // not visible at this point
dragShapes.rectangle, // not visible at this point
dragShapes.star, // not visible at this point
theShapeRect
);
stage.add(layer);
// Make the hit stage where we will do color sampling
const hitStage = new Konva.Stage({
container: "container2",
width: 300,
height: 300,
draggable: true
}),
hitLayer = new Konva.Layer(),
ctx = hitLayer.getCanvas().getContext(); // Get the convas context for access to pixel data
hitStage.add(hitLayer);
// Make an HTML image variable to act as the image loader, load the image
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
imageShape.image(img); // when loaded give the image to the Konva image shape
};
img.src = "https://assets.codepen.io/255591/map_of_wombania2.svg"; // start image loading - fires onload above.
// draw a small grey dot centered on each test point
for (const [key, pt] of Object.entries(pointsList)) {
layer.add(
pointCircle.clone({
name: key + " point",
radius: 5,
x: pt.x,
y: pt.y
})
);
}
// Function to get the color data for given point on a given canvas context
function getRGBAInfo(ctx, point) {
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba =
"pt " +
JSON.stringify(point) +
` rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
// console.log(rgba);
return data;
}
// function to reset collided point colors
function clearPoints() {
// clear the collision point colors
const points = stage.find(".point");
for (const point of points) {
point.fill("silver");
}
}
// variable to track whether we collided or not.
let hit = false;
// user clicks a shape-select button
$(".shapeButton").on("click", function () {
setShape($(this).data("shape"));
});
// Set the active shape.
function setShape(shapeName) {
clearPoints();
if (theShape) {
theShape.visible(false);
}
theShape = dragShapes[shapeName];
// Somewhere in Wombania....
theShape.position({
x: 300,
y: 120
});
// finally we see the shape !
theShape.visible(true);
// and set the bounding rect visualising rect
theShapeRect.position(theShape.getClientRect());
theShapeRect.size(theShape.getClientRect());
// better clear any listeners on the shape just in case
theShape.off();
// fires once as the drag commences
theShape.on("dragstart", function (evt) {
// clear the hitLayer for color testing
hitLayer.destroyChildren();
// make a copy of the dragging shape, positioned at top-left of hit canvas
// Note I fill shape with solid color - if you drag a Konva.Group then make a filled rect
// the pos & size of the group.getClientRect and add that into the group after cloning.
const clone = evt.target.clone({ fill: "red", stroke: "red" });
clone.position({
x: clone.width() / 2,
y: clone.height() / 2
});
hitLayer.add(clone);
// cloning copies some events so better clear them as they are not needed on the clone.
clone.off();
// reset the boundary point color
clearPoints();
// position the client rect visulaiser
theShapeRect.position(theShape.getClientRect());
theShapeRect.size(theShape.getClientRect());
});
// Will run on each drag move event
theShape.on("dragmove", function (evt) {
// assume no collisions - we will know by the end of the event
hit = false;
// position the client rect visulaiser
theShapeRect.position(theShape.getClientRect());
// Get the translation vector from the drag shape in the main canvas to the location
// in the hit canvas. We use thit to translate the check points in the main canvas
// to their positions in the hit canvas
const translateDist = {
x: -this.position().x + this.width() / 2,
y: -this.position().y + this.width() / 2
};
// get a rect around the current pos of the draggging shape, use to check if points
// are within this rect. If YES then process them, otherwise ignore.
const checkRect = this.getClientRect();
// Walk the set of check points...
for (const [key, pt] of Object.entries(pointsList)) {
// Is this point in the client rect of the dragging shape ?...
if (
checkRect.x < pt.x &&
checkRect.y < pt.y &&
checkRect.x + checkRect.width > pt.x &&
checkRect.y + checkRect.height > pt.y
) {
//...yes - so we pocess it
// translate the point to its position in the hit canvas.
let pointTranslated = {
x: pt.x + translateDist.x,
y: pt.y + translateDist.y
};
// get the color info of the point
const colorInfo = getRGBAInfo(ctx, pointTranslated);
// Is there any color there, anything, at all, maybe ?
if (colorInfo[0] + colorInfo[1] + colorInfo[2] + colorInfo[3] > 0) {
// if we find color then we have a collision!
hit = true;
// set the color of the collided point to visualise it
stage.findOne("." + key).fill("black");
// !Important: In live code we could 'break' here because it is not
// important to know _all_ the hits. I will process them all for demo purposes.
// break;
}
}
}
// Phew - after all that point fettling, if we got a hit then say so !
if (hit) {
$("#alarm").html("Boundary collision");
evt.target.fill("red");
} else {
evt.target.fill("lime");
$("#alarm").html("Still good");
}
});
}
body {
margin: 10px;
background-color: #f0f0f0;
}
.container {
border: 1px solid black;
display: inline-block;
}
alarm {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<p><span id='info'>Pick a shape, drag it around the country without hitting the edges!</span></p>
<p><span id='alarm'>.</span></p>
<p>
<button class="shapeButton" data-shape='circle'>Circle</button>
<button class="shapeButton" data-shape='rectangle'>Rectangle</button>
<button class="shapeButton" data-shape='star'>Star</button>
</span></p>
<div id="container" class='container'></div>
<div id="container2" class='container'></div>
Option #3: Alpha value checking.
The gist of this method is to have the color fill of each country have a specific alpha value in its RGBA setting. You can then check the colors at specific points on the perimeter of your dragging shape. Lets say we set the alpha for France to 250,
the Channel is 249, Spain 248, Italy 247, etc. If you are dragging your shape around 'inside' France, you expect an alpha value of 250. If you see anything else under any of those perimeter points then some part of your shape has crossed the border. [In practice, the HTML canvas will add some antialiasing along the border line so you will see some values outside those that you set but these have a low impact affect and can be ignored.]
One point is that you can't test the color on the main canvas if the shape being dragged is visible - because you will be getting the fill, stroke, or antialised pixel color of the shape!
To solve this you need a second stage - this can be memory only, so not visible on the page - where you load either a copy of the main stage with the dragging shape invisible, or you load the image of the map only. Let's call this the hit-stage. Assuming you keep the position of the hit-stage in line with the main-stage, then everything will work. Based on the location of the dragging shape and its perimeter points, you check the pixel colors on the hit-canvas. If the values match the country you are expecting then no hit, but if you see a different alpha value then you hit or passed the border. Actually you don't even need to know the color for the starting country - just note the color under the mouse point when the drag commences and look out for a different alpha value under the perimeter points.
There's a working demo of the 2-stage approach at codePen here. The demo just uses a country boundary and 'elsewhere' but you would use the same technique to construct an atlas of countries with different alpha values for your needs.
This is the JavaScript from the codepen demo. Best seen in full screen though when I checked it after copying from codepen some of the detections on right hand side did not fire, so maybe view the codepen if you can.
const countries = [
{ name: "wombania", alpha: 252 },
// add more countries as required
{ name: "Elsewhere", alpha: 0 }
],
scale = 1,
stage = new Konva.Stage({
container: "container",
width: 500,
height: 400,
draggable: false,
scale: {
x: scale,
y: scale
}
}),
layer = new Konva.Layer({
draggable: false
}),
imageShape = new Konva.Image({
x: 0,
y: 0,
draggable: false
}),
circle = new Konva.Circle({
radius: 30,
fill: "lime",
draggable: true,
x: 300,
y: 120,
scale: {
x: scale,
y: scale
}
});
let currentCountry = undefined;
const hitStage = new Konva.Stage({
container: "container2",
width: 500,
height: 400,
draggable: false
}),
hitLayer = new Konva.Layer(),
hitImage = new Konva.Image(),
ctx = hitLayer.getCanvas().getContext(); // Get the convas context for access to pixel data
layer.add(imageShape, circle);
stage.add(layer);
hitLayer.add(hitImage);
hitStage.add(hitLayer);
// Make an HTML image variable to act as the image loader, load the image
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
imageShape.image(img); // when loaded give the image to the Konva image shape
hitImage.image(img); // and to the hit canvas
const hitImageObj = new Image();
};
img.src = "https://assets.codepen.io/255591/map_of_wombania2.svg"; // start image loading - fires onload above.
// Will run on each drag move event
circle.on("dragmove", function () {
// get 20 points on the perimeter to check.
let hitCountry = currentCountry;
for (let angle = 0; angle < 360; angle = angle + 18) {
const angleRadians = (angle * Math.PI) / 180;
let point = {
x: parseInt(
circle.position().x + Math.cos(angleRadians) * circle.radius(),
10
),
y: parseInt(
circle.position().y + Math.sin(angleRadians) * circle.radius(),
10
)
};
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
// console.log("color at (" + point.x + ", " + point.y + "):", rgba);
// Here comes the good part.
// We know the alpha value for the current country - any other value means
// we crossed the border!
let country = getCountryAtPoint(point);
if (country && country.name !== currentCountry.name) {
hitCountry = country;
break; // jump out of the loop now because we know we got a hit.
}
}
// After checking the points what did the hit indicator show ?
if (hitCountry.alpha !== currentCountry.alpha) {
circle.fill("magenta");
$("#alarm").html("You crossed the border into " + hitCountry.name);
} else {
circle.fill("lime");
$("#alarm").html("Still inside " + hitCountry.name);
}
});
function getRGBAInfo(ctx, point) {
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
return data;
}
imageShape.on("mousemove", function () {
const point = stage.getPointerPosition();
getRGBAInfo(ctx, point);
});
function getCountryAtPoint(point) {
const colorInfo = getRGBAInfo(ctx, point);
for (const country of countries) {
if (country.alpha === colorInfo[3]) {
$("#info2").html("Selected: " + country.name);
return country;
}
}
}
imageShape.on("mousedown", function () {
currentCountry = getCountryAtPoint(stage.getPointerPosition());
});
circle.on("mousedown", function () {
currentCountry = getCountryAtPoint(stage.getPointerPosition());
});
body {
margin: 10px;
background-color: #f0f0f0;
}
.container {
border: 1px solid black;
display: inline-block;
}
alarm {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<p><span id='info'>Drag the circle around the country without hitting the edges!</span></p>
<p><span id='info2'>Selected: none</span> <span id='alarm'></span></p>
<div id="container" class='container'></div>
<div id="container2" class='container'></div>
PS. As a bonus, knowing the alpha values of the countries gives you an instant way to know which country the user clicks on. See the mousedown event.
To use an image, you can use the drawImage method of the canvas context in the clipFunc
const image = new Image();
image.src = 'image.png';
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.drawImage(image, 0, 0);
},
});
To use an SVG path, you can use the clip method of the canvas context in the clipFunc
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.clip('M10,10 h80 v80 h-80 Z');
},
});

JSXGraph - Animation of Polygon slow and rough

In JSXGraph I graph a plate (polygon) with 4 forces (arrows) acting on it. I intend to use the code for a variable number of forces (that's why it is more general than necessary for just 4 forces).
Force components can be shown and hidden. When a force arrow (not passing through the pivot point) is dragged, the "drag" event triggers an animation:
1st: showing the force and its arm, waiting for 2s
2nd: rotate the polygon in the direction of the force component (either clockwise or anti-clockwise).
Problem 1: The animation is slow and rough. I tried to use "suspendUpdate" and "unsuspendUpdate", but it made things even worse. Probably, I did not use it at the right place.
Problem 2: (probably related) I did not find a good way to clone the polygon for the animation. Or would it be best to store its coordinates? But if so, how would I reset the polygon (plus force arrow) to its original position after the rotation/animation?
My complete code is here: https://jsfiddle.net/bujowi/j1etgLsp/3/
You find the function called for the animation from line 244 onwards:
function animateRotation(x1, y1, x2, y2, sign)
Any comments and suggestions for improvements are welcome.
In the jsfiddle the animation appears to be rough since the the drag event is fired multiple times. Therefore, the animation runs in parallel multiple times. In the code below this is prevented by the flag is_animated. If true no new animation can be started.
A minor, more JSXGraph-ish issue is that the transformation is added to the JSXGraph elements in every step of the animation in setInterval. This may produce strange effects. It is much more clean to bind the transformation once to an element, but make the transformation dynamic, e.g. dependent on an angle variable:
var angle = 0.03;
let tRot = b1.create('transform', [function() { return sign * angle; }, pivot], { type: 'rotate' });
Additionally, this would make it possible to rotate the polygon back to the zero angle at the end of the animation. So, in setInterval only the angle variable is increased and the board is updated:
let idInterval = setInterval(function() {
angle += 0.03;
b1.update();
// ...
}, refreshTime);
Here is the complete new animation code (without rotating back at the end):
function animateRotation(x1, y1, x2, y2, sign) {
if (this.is_animated) {
return;
}
this.is_animated = true;
// Force arrow and arm (distance from pivot)
let Rot1 = b1.create('point', [x1, y1], { visible: false })
let Rot2 = b1.create('point', [x2, y2], { visible: false })
let rotArrow = b1.create('arrow', [Rot1, Rot2], { strokeColor: '#000', strokeWidth: 3, visible: true })
// Polygon
let PolyCopy = []
for (i = 0; i < poly_coord.length; i++) {
PolyCopy.push(b1.create('point', [poly_coord[i][0], poly_coord[i][1]], at))
}
let polyCopy = b1.create('polygon', PolyCopy, { strokeColor: myBlue, fillColor: myBlue })
// Hide all other force arrows
showAllElements(false)
// Show line of action and distance from pivot for 1s
let lineOfAction = b1.create('line', [[x1, y1], [x2, y2]], { strokeColor: '#000', strokeWidth: 2, dash: 1 })
let perpLine = b1.create('perpendicular', [lineOfAction, Pivot], { withLabel: false, visible: false })
let isecP = b1.create('intersection', [perpLine, lineOfAction, 0], { withLabel: false, visible: false })
let dist = b1.create('segment', [Pivot, isecP], { strokeColor: '#000', strokeWidth: 4 })
var that = this; // make "this" available in the interval function
// This is the animation: rotating the polygon for how many 'frames', refreshTime in ms
setTimeout(function() {
b1.removeObject(lineOfAction)
b1.removeObject(perpLine)
b1.removeObject(isecP)
b1.removeObject(dist)
var angle = 0.03;
let tRot = b1.create('transform', [function() { return sign * angle; }, pivot], { type: 'rotate' });
tRot.bindTo(PolyCopy);
tRot.bindTo([Rot1, Rot2]);
let frames = 0;
const refreshTime = 50;
let idInterval = setInterval(function() {
angle += 0.03;
b1.update();
if (frames++ > 20) {
that.is_animated = false;
b1.removeObject(polyCopy);
b1.removeObject(rotArrow);
showAllElements(true);
clearInterval(idInterval);
}
}, refreshTime)
}, 1000); // time for showing the line of action and distance
}
See it live at https://jsfiddle.net/fbdzx0ht/3/
Best wishes,
Alfred

Dynamically generated moveable vector shapes in Paper.js

I'm trying to render an arrow type shape in Paper.js. I have been able to create the segments that render out the tip of the arrow, but have been unable to create any further points that would finish the outline of the arrow. For my own testing purposes, it's currently only 3 lines, however I need to create a shape that can have fills, etc, so I need to be able to outline the arrow and have the group move dynamically when the mouse is dragged in a direction. I need a fat arrow!
Every point I choose, despite being relative to the position of the current vector, seem to rotate on their own when the arrow is manipulated.
Been hitting my head against this for days with no luck.
Here's what I'm working with -
var vectorStart, vector;
var vectorItem = new Group();
onMouseDrag = function (event) {
var arrowLength = 50;
vectorItem.remove();
engaged = true;
vectorStart = view.center;
var end = vectorStart + vector;
vector = event.point - vectorStart;
console.log('arrow pointer location: ' + event.point);
var vectorArrow = vector.normalize(arrowLength);
vectorItem = new Group([
new Path([vectorStart, end]),
new Path([
end + vectorArrow.rotate(120),
end,
end + vectorArrow.rotate(-120),
]),
]);
vectorItem.strokeWidth = 1;
vectorItem.strokeColor = 'black';
this.onMouseUp = function() {
vectorItem.remove();
}
}
Here's a link to a Sketch containing my code.
What I'm not understanding is how to add points to the Path that generates the arrow in order to create a shape. Everything seems to rotate on it's own and doesn't behave in the way that I need it to.
Any help would be great!
A simple way of drawing an arrow outline is by combining 3 rectangles.
Paper.js allow you to do this with Path.unite() method.
Here is an overview of the drawing algorithm
Here is a Sketch demonstrating my solution.
//
// CONSTANTS
//
// user defined
var STROKE_WIDTH = 40;
var HEAD_LENGTH = 300;
var STYLE = {
fillColor : 'orange',
strokeColor: 'black',
strokeWidth: 5
};
// computed
var WIDTH = STROKE_WIDTH * 2;
var DIAGONAL = Math.sqrt(Math.pow(STROKE_WIDTH * 2, 2) * 2);
//
// METHODS
//
/**
* Draws an arrow between two points.
* For simplicity sake, arrow is drawn horizontally at origin first
* then it is moved and rotated according to start / end points.
* It is composed of 3 rectangles which are united into a single shape.
* #param {Point} start
* #param {Point} end
*/
function drawArrow(start, end)
{
// calculate distance between points
var distance = start.getDistance(end);
// make sure it is not lower than diagonal
if (distance < DIAGONAL)
{
distance = DIAGONAL;
}
// draw rectangles
var directionRectangle = new Path.Rectangle(new Point(0, -STROKE_WIDTH), new Point(distance - DIAGONAL, STROKE_WIDTH));
var topRectangle = new Path.Rectangle(new Point(0, -STROKE_WIDTH), new Point(HEAD_LENGTH, STROKE_WIDTH));
// move top rectangle to the right
topRectangle.translate(directionRectangle.bounds.rightCenter - topRectangle.bounds.rightCenter + [ WIDTH, 0 ]);
// make bottom rectangle by cloning top one
var bottomRectangle = topRectangle.clone();
// offset top and bottom rectangles
topRectangle.position -= [ 0, STROKE_WIDTH ];
bottomRectangle.position += [ 0, STROKE_WIDTH ];
// rotate then to form arrow head
topRectangle.rotate(45, topRectangle.bounds.bottomRight - [ WIDTH, 0 ]);
bottomRectangle.rotate(-45, bottomRectangle.bounds.topRight - [ WIDTH, 0 ]);
// join the 3 rectangles into one path
var arrow = directionRectangle.unite(topRectangle).unite(bottomRectangle);
// move and rotate this path to fit start / end positions
arrow.translate(start - directionRectangle.bounds.leftCenter);
arrow.rotate((end - start).angle, start);
// apply custom styling
arrow.style = STYLE;
// remove construction items
directionRectangle.remove();
topRectangle.remove();
bottomRectangle.remove();
}
function onMouseDrag(event)
{
// clear canvas
project.clear();
// draw arrow according to mouse position
drawArrow(event.downPoint, event.point);
}
//
// INIT
//
// display instructions
new PointText({
point : view.center,
justification: 'center',
content : 'Draw arrow by dragging and dropping with your mouse.'
});
Here's some code that creates an arrow. The object is initialized with mouse down point and draws an arrow with the tip at the point the mouse is dragged to.
function Arrow (mouseDownPoint) {
this.start = mouseDownPoint;
this.headLength = 20;
this.tailLength = 9;
this.headAngle = 35;
this.tailAngle = 110
}
Arrow.prototype.draw = function (point) {
var end = point;
var arrowVec = this.start.subtract(end);
// parameterize {headLength: 20, tailLength: 6, headAngle: 35, tailAngle: 110}
// construct the arrow
var arrowHead = arrowVec.normalize(this.headLength);
var arrowTail = arrowHead.normalize(this.tailLength);
var p3 = end; // arrow point
var p2 = end.add(arrowHead.rotate(-this.headAngle)); // leading arrow edge angle
var p4 = end.add(arrowHead.rotate(this.headAngle)); // ditto, other side
var p1 = p2.add(arrowTail.rotate(this.tailAngle)); // trailing arrow edge angle
var p5 = p4.add(arrowTail.rotate(-this.tailAngle)); // ditto
// specify all but the last segment, closed does that
this.path = new paper.Path(this.start, p1, p2, p3, p4, p5);
this.path.closed = true;
this.path.strokeWidth = 1
this.path.strokColor = 'black'
this.path.fillColor = 'black'
return this.path
}
I like the tapered tail but you can get rid of that by fiddling with the constructor lengths.
Here's a sketch with the mouse handling

Draw a circle of constant size for all zoom levels leaflet.js

I am trying to implement something like this using leaflet.js, where the size of the circle remains the same on varying zoom levels. For example, if I want to depict the populations in different US counties, I would have circles of different radius represent different ranges of populations. They may overlap when zoomed out completely, but once we start zooming in, they tend to separate. So is there a way to do this using leaflet.js. I saw an issue raised, but I wasn't able to follow if it was fixed or not. Any help would be deeply appreciated.
For circles, just use circleMarker instead of circle: http://leafletjs.com/reference.html#circlemarker
I think you're gonna want to do something like this:
var map = L.map('map').setView([51.505, -0.09], 13);
var circle = L.circle([51.508, -0.11], 500, {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5
}).addTo(map);
var myZoom = {
start: map.getZoom(),
end: map.getZoom()
};
map.on('zoomstart', function(e) {
myZoom.start = map.getZoom();
});
map.on('zoomend', function(e) {
myZoom.end = map.getZoom();
var diff = myZoom.start - myZoom.end;
if (diff > 0) {
circle.setRadius(circle.getRadius() * 2);
} else if (diff < 0) {
circle.setRadius(circle.getRadius() / 2);
}
});
What I've done is simply initialize a map and a circle and created event listeners for the zoomstart and zoomend events. There's a myZoom object that records the zoom levels so you can find out whether or not the final zoom is in or out by simple subtraction. In the zoomEnd listener, you check that and change the circle radius based on whether the difference is greater or lesser than 0. We of course do nothing when it's 0. This is where I leave you to get more sophisticated with your results. But, I think this demonstrates how to do it.
Try this if you can't use circle markers for some reason:
// Determine the number of meters per pixel based on map zoom and latitude
const zoom = map.getZoom()
const lat = map.getCenter().lat
const metersPerPixel = 156543.03392 * Math.cos(lat * Math.PI / 180) / Math.pow(2, zoom)
// Multiply that by a factor based on how large the circle should appear on the screen
const radius = metersPerPixel * desiredRadiusInPixels
// Create the circle
const circle = L.circle(this.map.getCenter(), {radius: radius}))
I found the "meters per pixel" formula in this answer:
https://gis.stackexchange.com/a/127949/106283
Otherwise you can do like this, by adapting the exponent of the Math.pow() according to your needs
mymap.on('zoomend', function (e) {
var newRadius = Math.pow((20 - mymap.getZoom()), 4);
myCircle.setRadius(newRadius);
});

raphaelJS 2.1 animate along path

I want to animate a path (actually a set of paths, but I'll get to that) along a curved path.
RaphaelJS 2 removed the animateAlong method, for reasons I haven't been able to discern. Digging into the Raphael documentation's gears demo as abstracted by Zevan, I have got this far:
//adding a custom attribute to Raphael
(function() {
Raphael.fn.addGuides = function() {
this.ca.guide = function(g) {
return {
guide: g
};
};
this.ca.along = function(percent) {
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "t" + [point.x, point.y]
};
return t;
};
};
})();
var paper = Raphael("container", 600, 600);
paper.addGuides();
// the paths
var circ1 = paper.circle(50, 150, 40);
var circ2 = paper.circle(150, 150, 40);
var circ3 = paper.circle(250, 150, 40);
var circ4 = paper.circle(350, 150, 40);
var arc1 = paper.path("M179,204c22.667-7,37,5,38,9").attr({'stroke-width': '2', 'stroke': 'red'});
// the animation
// works but not at the right place
circ3.attr({guide : arc1, along : 1})
.animate({along : 0}, 2000, "linear");
http://jsfiddle.net/hKGLG/4/
I want the third circle to animate along the red path. It is animating now, but at a distance from the red path equal to the third circle's original coordinates. The weird thing is that this happens whether the transform translate in the along object is relative (lowercase "t") or absolute (uppercase "T"). It also always animates in the same spot, even if I nudge it with a transform translation just before the animate call.
Any help very appreciated. I just got off the boat here in vector-land. Pointers are helpful--a working fiddle is even better.
You're just a hop, skip, and jump away from the functionality that you want. The confusion here concerns the interaction between transformations and object properties -- specifically, that transformations do not modify the original object properties. Translating simply adds to, rather than replaces, the original coordinates of your circles.
The solution is extremely straightforward. In your along method:
this.ca.along = function(percent) {
var box = this.getBBox( false ); // determine the fundamental location of the object before transformation occurs
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "...T" + [point.x - ( box.x + ( box.width / 2 ) ), point.y - ( box.y + ( box.height / 2 ) )] // subtract the center coordinates of the object from the translation offset at this point in the guide.
};
return t;
Obviously, there's some room for optimization here (i.e., it might make sense to create all your circles at 0,0 and then translate them to the display coordinates you want, avoiding a lot of iterative math). But it's functional... see here.
One other caveat: the ...T translation won't effect any other transforms that have already been applied to a given circle. This implementation is not guaranteed to play nicely with other transforms.

Categories

Resources