Snap.svg: How can I skew an element? - javascript

I am new to Snap.svg and I am trying to skew a simple rectangle, but I can't figure out how. I already searched in the docs.
This is what I have so far:
/* stage */
var s = Snap('#mysvg');
s.clientRect = s.node.getBoundingClientRect();
s.width = s.clientRect.width;
s.height = s.clientRect.height;
s.center = {
"left" : s.width/2,
"top" : s.height/2,
};
/* rectangle */
var rect = {};
rect.width = 120;
rect.height = 230;
rect.borderRadius = 10;
rect = s.rect(s.center.left, s.center.top,rect.width,rect.height, rect.borderRadius);
rect.transformMatrix = new Snap.Matrix();
rect.transformMatrix.scale(1,0.86062);
rect.transformMatrix.rotate(30);
// rect.transformMatrix.skew(30);
rect.transform(rect.transformMatrix);
It seems like skewing is not supported within the transform Matrix..
any ideas?

Snap.svg does not have a skew function included by default.
You could add a custom skew function as a plugin.
This function will skew from the centre. You can remove the bbox code if you don't need it, it will take 0,0 as a centre).
jsfiddle
Snap.plugin( function( Snap, Element, Paper, global ) {
Element.prototype.skew = function( angleX, angleY ) {
var bbox = this.getBBox();
var m = new Snap.Matrix( 1, Snap.rad(angleY), Snap.rad(angleX), 1, 0, 0);
var dx = m.x( bbox.cx, bbox.cy ) - bbox.cx;
var dy = m.y( bbox.cx, bbox.cy ) - bbox.cy;
m.translate( -dx, -dy )
this.transform( m );
};
});
var s = Snap("#svg");
var block = s.rect(100, 100, 100, 100);
block.skew(90,0); // try (0,90 for skewY)

From my own personal experience with using Snap for very mathematically intense animations on multiple elements at a time, where performance matters, Snap's built in methods are very slow and do a lot of string operations and loops. Its better to just directly manipulate the DOM element's matrix, rather than the Snap element's matrix. Ideally, you would have the matrix stored somewhere and only declared once, but getting it every time you make a change is fine also. The following code contains the skew stuff you are asking for. I find things to be way faster and more convenient when doing things this way:
Snap.plugin( function( Snap, Element, Paper, global ) {
Element.prototype.getMatrix = function( ) {
return this.node.transform.baseVal.getItem(0).matrix;
};
//angles in degrees
Element.prototype.skew = function( angleX, angleY ) {
var m = this.getMatrix();
m.b = Math.tan(angleY*Math.PI/180)
m.c = Math.tan(angleX*Math.PI/180)
};
Element.prototype.translate = function( x, y ) {
var m = this.getMatrix();
m.e = x;
m.f = y;
};
Element.prototype.scale = function( x, y ) {
var m = this.getMatrix();
m.a = x;
m.d = y;
};
Element.prototype.rotate = function( degrees) {
var m = this.getMatrix();
var a = degrees*Math.PI/180;
m.a = Math.cos(a);
m.b = Math.sin(a);
m.c = -Math.sin(a);
m.d = Math.cos(a);
};
});
So to use this code, assuming 'rect' is the variable name for the Snap element you created, you would type:
//sets transform to x, y = 100, 100
rect.translate(100, 100);
// scales x to 1 and y to 0.5
rect.scale(1, 0.5);
//skews 30 degrees horizontally and 90 degrees vertically
rect.skew(30, 90);

Related

Box2dWeb: Object Rotation - GetWorldCenter not found

I'm trying to make a function for a skydiving game that'll make a sprite rotate left/right and then move that way depending on how angled it is, at least i would be if the object's get world centre parameter was recognised, code is as follows:
Dynamic circle creation:
function defineNewDynamicCircle(density, friction, restitution, x, y, r, objid) {
var fixDef = new b2FixtureDef;
fixDef.density = density;
fixDef.friction = friction;
fixDef.restitution = restitution;
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_dynamicBody;
bodyDef.position.x = x / SCALE;
bodyDef.position.y = y / SCALE;
// This creates a new circle shape
fixDef.shape = new b2CircleShape(r/SCALE);
var thisobj = world.CreateBody(bodyDef).CreateFixture(fixDef);
thisobj.GetBody().SetUserData({id:objid})
return thisobj;
}
object initialisation:
var capsule = defineNewDynamicCircle(1.0,0.2,0.8,400,400,20,"capsule");
problematic function:
function bankleft() {
capsule.GetBody.ApplyImpulse(new b2Vec2(-0.5,0), capsule.GetBody.GetWorldCenter());
if(capsule.GetBody.GetLinearVelocity().x < -5) {
capsule.GetBody.SetLinearVelocity(new b2Vec2(-5,capsule.GetBody.GetLinearVelocity().y));
}
}
Thanks for any insights you can provide!

How to use Three.InstancedMesh in Aframe

I'm trying to implement instancing in Aframe using the ThreeJs InstancedMesh based on the example here: https://github.com/mrdoob/three.js/blob/master/examples/webgl_instancing_dynamic.html
Relevant section of code here:
init: function() {
const {count, radius, scale, colors, positions} = this.data;
this.start = true;
this.dummy = new THREE.Object3D();
this.count = count;
this.startObject = new THREE.Object3D();
this.endObject = new THREE.Object3D();
this.instanceColors = new Float32Array(count * 3);
this.instanceColorsBase = new Float32Array(this.instanceColors.length);
this.vertices = [];
this.rotations = [];
for ( var i = 0; i < this.data.count; i ++ ) {
var x = this.data.positions[i][0] * this.data.scale;
var y = this.data.positions[i][1] * this.data.scale;
var z = this.data.positions[i][2] * this.data.scale;
var xEnd = x + this.data.endPositions[i][0] * this.data.scale;
var yEnd = y + this.data.endPositions[i][1] * this.data.scale;
var zEnd = z + this.data.endPositions[i][2] * this.data.scale;
this.vertices.push( x, y, z );
const rotation = this.getDirection({'x':x,'y':y,'z':z},
{'x':xEnd,'y':yEnd,'z':zEnd});
this.rotations.push(rotation.x, rotation.y, rotation.z);
}
let mesh;
let geometry;
let material;
const loader = new THREE.GLTFLoader();
const el = this.el;
loader.load("/assets/arrow/arrow.gltf", function ( model ) {
geometry = model.scene.children[0].children[0].geometry;
geometry.computeVertexNormals();
geometry.scale( 0.03, 0.03, 0.03 );
material = new THREE.MeshNormalMaterial();
mesh = new THREE.InstancedMesh( geometry, material, count );
mesh.instanceMatrix.setUsage( THREE.DynamicDrawUsage );
el.object3D.add(mesh);
} );
this.el.setAttribute("id", "cells");
},
setMatrix: function (start) {
if (this.mesh) {
for ( let i = 0; i < this.count; i ++ ) {
var x = this.data.positions[i][0] * this.data.scale;
var y = this.data.positions[i][1] * this.data.scale;
var z = this.data.positions[i][2] * this.data.scale;
var xEnd = x + this.data.endPositions[i][0] * this.data.scale;
var yEnd = y + this.data.endPositions[i][1] * this.data.scale;
var zEnd = z + this.data.endPositions[i][2] * this.data.scale;
if (start) {
this.dummy.position.set(xEnd, yEnd, zEnd);
} else {
this.dummy.position.set(x, y, z);
}
this.dummy.rotation.x = this.rotations[i][0];
this.dummy.rotation.y = this.rotations[i][1];
this.dummy.rotation.z = this.rotations[i][2];
this.dummy.updateMatrix();
this.mesh.setMatrixAt( i, this.dummy.matrix );
}
this.mesh.instanceMatrix.needsUpdate = true;
}
}
tick: function() {
this.setMatrix(this.start);
this.start = !this.start;
},
No errors or relevant messages that I can see, but none of the instanced objects are rendering. I don't really have a good way to post an example unfortunately. Anyone know what I'm doing wrong? Thanks in advance!
Note: It seems that the objects are being rendered because the number of triangles being drawn increases drastically when I add this component. However, they are not visible anywhere and I can't find them in the aframe inspector either
It's a very case specific question with a quite extensive topic, so:
In general, using THREE.InstancedMeshes is simple, and you got it right:
// create an instanced mesh
let iMesh = new THREE.InstancedMesh(geometry, material, count)
element.object3D.add(iMesh)
// manipulate the instances
let mtx = new Matrix4()
// set the position, rotation, scale of the matrix
// ...
// update the instance
iMesh.setMatrixAt(index, mtx);
iMesh.instanceMatrix.needsUpdate = true;
Example of an instanced gltf model here
Your code is doing a lot, and it would be easier if it could be stripped to a bare minimum. Yet I think there is only one major issue - this.model isn't set anywhere, so the setMatrix function does nothing. Other than that you may need to disable frustum culling (mesh.frustumCulling = false), or set a bounding sphere - otherwise the objects may dissapear when the base object is out of sight.
Once it's set, your code seems to be working

Transform Rounded Rectangle to Circle

I've been working on a specific animation in which I need to convert(with animation) a Rounded Rectangle Shape to Circle. I've checked the documentation of paper.js and haven't found any predefined function to achieve this.
-->
The animation needs to be smooth. As the number of rectangles I'm working with is very high, I can't use the "remove current rounded rect and redraw one more rounded version" method. It reduces the performace and the animation gets laggy.
This is the code I'm using to generate rounded rectangle.
// Had to paste something to post the question
// Though the whole code can be seen on codepen link
var rect = new Rectangle();
var radius = 100, origin = {x: 100, y: 100};
rect.size = new Size(radius, radius);
rect.center = new Point(origin.x, origin.y);
var cornerSize = radius / 4;
var shape = new Path.Rectangle(rect, cornerSize);
Prepared this Codepen example to show the progress.
If we can work out the whole animation using any other object types, that will be fine too. For now I can't find any any property which can transform the rounded rectangle to circle.
I'm also animating color of the object and position. I've gone through many documents to find out color animation.
PS: If there is any other(better) technique to animate colors of object, please share that too.
You will first have to create a path as a rounded rectangle. Then with each step in your animation you have to modify the eight segments of the path. This will only work with Path objects, not if your rectangle is a Shape.
The segment points and the handles have to be set like this:
κ (kappa) is defined in paper.js as Numerical.KAPPA (more on Kappa here).
The code to change the radius could look like this (Click here for the Sketch):
var rect = new Path.Rectangle(new Point(100, 100), new Size(100, 100), 30);
rect.fullySelected = true;
var step = 1;
var percentage = 0;
function onFrame(event) {
percentage += step;
setCornerRadius(rect, percentage)
if (percentage > 50 || percentage < 0) {
step *= -1;
}
}
function setCornerRadius(rectPath, roundingPercent) {
roundingPercent = Math.min(50, Math.max(0, roundingPercent));
var rectBounds = rectPath.bounds;
var radius = roundingPercent/100 * Math.min(rectBounds.width, rectBounds.height);
var handleLength = radius * Numerical.KAPPA;
l = rectBounds.getLeft(),
t = rectBounds.getTop(),
r = rectBounds.getRight(),
b = rectBounds.getBottom();
var segs = rectPath.segments;
segs[0].point.x = segs[3].point.x = l + radius;
segs[0].handleOut.x = segs[3].handleIn.x = -handleLength;
segs[4].point.x = segs[7].point.x = r - radius;
segs[4].handleOut.x = segs[7].handleIn.x = handleLength;
segs[1].point.y = segs[6].point.y = b - radius;
segs[1].handleIn.y = segs[6].handleOut.y = handleLength;
segs[2].point.y = segs[5].point.y = t + radius;
segs[2].handleOut.y = segs[5].handleIn.y = -handleLength;
}
Edit: I just found a much easier way using a shape. Not sure which approach performs faster.
Here is the implementation using a Shape (Click here for the Sketch).
var size = 100;
var rect = new Shape.Rectangle(new Rectangle(new Point(100, 100), new Size(size, size)), 30);
rect.strokeColor = "red";
var step = 1;
var percentage = 0;
function onFrame(event) {
percentage = Math.min(50, Math.max(0, percentage + step));
rect.radius = size * percentage / 100;
if (percentage >= 50 || percentage <= 0) {
step *= -1;
}
}
Change the corner size to the following
var cornerSize = circle.radius / 1;

mapbox-gl-js: Adjust visible area & bearing to a given line, for a given pitch

I'm trying to optimize a Mapbox view for long-distance hiking trails, like the Appalachian Trail or the Pacific Crest Trail. Here's an example, which I've oriented by hand, showing the Senda Pirenáica in Spain:
The area of interest, the viewport, and the pitch are given. I need to find the correct center, bearing, and zoom.
The map.fitBounds method doesn't help me here because it assumes pitch=0 and bearing=0.
I've done some poking around and this seems to be a variation of the smallest surrounding rectangle problem, but I'm stuck on a couple of additional complications:
How do I account for the distorting effect of pitch?
How do I optimize for the aspect ratio of the viewport? Note that taking the viewport narrower or wider would change the bearing of the best solution:
FWIW I'm also using turf-js, which helps me get the convex hull for the line.
This solution results in the path displayed at the correct bearing with a magenta trapezoid outline showing the target "tightest trapezoid" to show the results of the calculations. The extra line coming from the top corner shows where the map.center() value is located.
The approach is as follows:
render the path to the map using the "fitbounds" technique to get an approximate zoom level for the "north up and pitch=0" situation
rotate the pitch to the desired angle
grab the trapezoid from the canvas
This result would look like this:
After this, we want to rotate that trapezoid around the path and find the tightest fit of the trapezoid to the points. In order to test for the tightest fit it is easier to rotate the path rather than the trapezoid so I have taken that approach here. I haven't implemented a "convex hull" on the path to minimize the number of points to rotate but that is something that can be added as an optimization step.
To get the tightest fit, the first step is to move the map.center() so that the path is at the "back" of the view. This is where the most space is in the frustum so it will be easy to manipulate it there:
Next, we measure the distance between the angled trapezoid walls and each point in the path, saving the closest points on both the left and right sides. We then center the path in the view by translating the view horizontally based on these distances, and then scale the view to eliminate that space on both sides as shown by the green trapezoid below:
The scale used to get this "tightest fit" gives us our ranking for whether this is the best view of the path. However, this view may not be the best visually since we pushed the path to the back of the view to determine the ranking. Instead, we now adjust the view to place the path in the vertical center of the view, and scale the view triangle larger accordingly. This gives us the magenta colored "final" view desired:
Finally, this process is done for every degree and the minimum scale value determines the winning bearing, and we take the associated scale and center position from there.
mapboxgl.accessToken = 'pk.eyJ1IjoiZm1hY2RlZSIsImEiOiJjajJlNWMxenowNXU2MzNudmkzMndwaGI3In0.ALOYWlvpYXnlcH6sCR9MJg';
var map;
var myPath = [
[-122.48369693756104, 37.83381888486939],
[-122.48348236083984, 37.83317489144141],
[-122.48339653015138, 37.83270036637107],
[-122.48356819152832, 37.832056363179625],
[-122.48404026031496, 37.83114119107971],
[-122.48404026031496, 37.83049717427869],
[-122.48348236083984, 37.829920943955045],
[-122.48356819152832, 37.82954808664175],
[-122.48507022857666, 37.82944639795659],
[-122.48610019683838, 37.82880236636284],
[-122.48695850372314, 37.82931081282506],
[-122.48700141906738, 37.83080223556934],
[-122.48751640319824, 37.83168351665737],
[-122.48803138732912, 37.832158048267786],
[-122.48888969421387, 37.83297152392784],
[-122.48987674713133, 37.83263257682617],
[-122.49043464660643, 37.832937629287755],
[-122.49125003814696, 37.832429207817725],
[-122.49163627624512, 37.832564787218985],
[-122.49223709106445, 37.83337825839438],
[-122.49378204345702, 37.83368330777276]
];
var myPath2 = [
[-122.48369693756104, 37.83381888486939],
[-122.49378204345702, 37.83368330777276]
];
function addLayerToMap(name, points, color, width) {
map.addLayer({
"id": name,
"type": "line",
"source": {
"type": "geojson",
"data": {
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": points
}
}
},
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": color,
"line-width": width
}
});
}
function Mercator2ll(mercX, mercY) {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var lon = mercX / shift * 180.0;
var lat = mercY / shift * 180.0;
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
return [ lon, lat ];
}
function ll2Mercator(lon, lat) {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var x = lon * shift / 180;
var y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
y = y * shift / 180;
return [ x, y ];
}
function convertLL2Mercator(points) {
var m_points = [];
for(var i=0;i<points.length;i++) {
m_points[i] = ll2Mercator( points[i][0], points[i][1] );
}
return m_points;
}
function convertMercator2LL(m_points) {
var points = [];
for(var i=0;i<m_points.length;i++) {
points[i] = Mercator2ll( m_points[i][0], m_points[i][1] );;
}
return points;
}
function pointsTranslate(points,xoff,yoff) {
var newpoints = [];
for(var i=0;i<points.length;i++) {
newpoints[i] = [ points[i][0] + xoff, points[i][1] + yoff ];
}
return(newpoints);
}
// note [0] elements are lng [1] are lat
function getBoundingBox(arr) {
var ne = [ arr[0][0] , arr[0][1] ];
var sw = [ arr[0][0] , arr[0][1] ];
for(var i=1;i<arr.length;i++) {
if(ne[0] < arr[i][0]) ne[0] = arr[i][0];
if(ne[1] < arr[i][1]) ne[1] = arr[i][1];
if(sw[0] > arr[i][0]) sw[0] = arr[i][0];
if(sw[1] > arr[i][1]) sw[1] = arr[i][1];
}
return( [ sw, ne ] );
}
function pointsRotate(points, cx, cy, angle){
var radians = angle * Math.PI / 180.0;
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var newpoints = [];
function rotate(x, y) {
var nx = cx + (cos * (x - cx)) + (-sin * (y - cy));
var ny = cy + (cos * (y - cy)) + (sin * (x - cx));
return [nx, ny];
}
for(var i=0;i<points.length;i++) {
newpoints[i] = rotate(points[i][0],points[i][1]);
}
return(newpoints);
}
function convertTrapezoidToPath(trap) {
return([
[trap.Tl.lng, trap.Tl.lat], [trap.Tr.lng, trap.Tr.lat],
[trap.Br.lng, trap.Br.lat], [trap.Bl.lng, trap.Bl.lat],
[trap.Tl.lng, trap.Tl.lat] ]);
}
function getViewTrapezoid() {
var canvas = map.getCanvas();
var trap = {};
trap.Tl = map.unproject([0,0]);
trap.Tr = map.unproject([canvas.offsetWidth,0]);
trap.Br = map.unproject([canvas.offsetWidth,canvas.offsetHeight]);
trap.Bl = map.unproject([0,canvas.offsetHeight]);
return(trap);
}
function pointsScale(points,cx,cy, scale) {
var newpoints = []
for(var i=0;i<points.length;i++) {
newpoints[i] = [ cx + (points[i][0]-cx)*scale, cy + (points[i][1]-cy)*scale ];
}
return(newpoints);
}
var id = 1000;
function convertMercator2LLAndDraw(m_points, color, thickness) {
var newpoints = convertMercator2LL(m_points);
addLayerToMap("id"+id++, newpoints, color, thickness);
}
function pointsInTrapezoid(points,yt,yb,xtl,xtr,xbl,xbr) {
var str = "";
var xleft = xtr;
var xright = xtl;
var yh = yt-yb;
var sloperight = (xtr-xbr)/yh;
var slopeleft = (xbl-xtl)/yh;
var flag = true;
var leftdiff = xtr - xtl;
var rightdiff = xtl - xtr;
var tmp = [ [xtl, yt], [xtr, yt], [xbr,yb], [xbl,yb], [xtl,yt] ];
// convertMercator2LLAndDraw(tmp, '#ff0', 2);
function pointInTrapezoid(x,y) {
var xsloperight = xbr + sloperight * (y-yb);
var xslopeleft = xbl - slopeleft * (y-yb);
if((x - xsloperight) > rightdiff) {
rightdiff = x - xsloperight;
xright = x;
}
if((x - xslopeleft) < leftdiff) {
leftdiff = x - xslopeleft;
xleft = x;
}
if( (y<yb) || (y > yt) ) {
console.log("y issue");
}
else if(xsloperight < x) {
console.log("sloperight");
}
else if(xslopeleft > x) {
console.log("slopeleft");
}
else return(true);
return(false);
}
for(var i=0;i<points.length;i++) {
if(pointInTrapezoid(points[i][0],points[i][1])) {
str += "1";
}
else {
str += "0";
flag = false;
}
}
if(flag == false) console.log(str);
return({ leftdiff: leftdiff, rightdiff: rightdiff });
}
var viewcnt = 0;
function calculateView(trap, points, center) {
var bbox = getBoundingBox(points);
var bbox_height = Math.abs(bbox[0][1] - bbox[1][1]);
var view = {};
// move the view trapezoid so the path is at the far edge of the view
var viewTop = trap[0][1];
var pointsTop = bbox[1][1];
var yoff = -(viewTop - pointsTop);
var extents = pointsInTrapezoid(points,trap[0][1]+yoff,trap[3][1]+yoff,trap[0][0],trap[1][0],trap[3][0],trap[2][0]);
// center the view trapezoid horizontally around the path
var mid = (extents.leftdiff - extents.rightdiff) / 2;
var trap2 = pointsTranslate(trap,extents.leftdiff-mid,yoff);
view.cx = trap2[5][0];
view.cy = trap2[5][1];
var w = trap[1][0] - trap[0][0];
var h = trap[1][1] - trap[3][1];
// calculate the scale to fit the trapezoid to the path
view.scale = (w-mid*2)/w;
if(bbox_height > h*view.scale) {
// if the path is taller than the trapezoid then we need to make it larger
view.scale = bbox_height / h;
}
view.ranking = view.scale;
var trap3 = pointsScale(trap2,(trap2[0][0]+trap2[1][0])/2,trap2[0][1],view.scale);
w = trap3[1][0] - trap3[0][0];
h = trap3[1][1] - trap3[3][1];
view.cx = trap3[5][0];
view.cy = trap3[5][1];
// if the path is not as tall as the view then we should center it vertically for the best looking result
// this involves both a scale and a translate
if(h > bbox_height) {
var space = h - bbox_height;
var scale_mul = (h+space)/h;
view.scale = scale_mul * view.scale;
cy_offset = space/2;
trap3 = pointsScale(trap3,view.cx,view.cy,scale_mul);
trap3 = pointsTranslate(trap3,0,cy_offset);
view.cy = trap3[5][1];
}
return(view);
}
function thenCalculateOptimalView(path) {
var center = map.getCenter();
var trapezoid = getViewTrapezoid();
var trapezoid_path = convertTrapezoidToPath(trapezoid);
trapezoid_path[5] = [center.lng, center.lat];
var view = {};
//addLayerToMap("start", trapezoid_path, '#00F', 2);
// get the mercator versions of the points so that we can use them for rotations
var m_center = ll2Mercator(center.lng,center.lat);
var m_path = convertLL2Mercator(path);
var m_trapezoid_path = convertLL2Mercator(trapezoid_path);
// try all angles to see which fits best
for(var angle=0;angle<360;angle+=1) {
var m_newpoints = pointsRotate(m_path, m_center[0], m_center[1], angle);
var thisview = calculateView(m_trapezoid_path, m_newpoints, m_center);
if(!view.hasOwnProperty('ranking') || (view.ranking > thisview.ranking)) {
view.scale = thisview.scale;
view.cx = thisview.cx;
view.cy = thisview.cy;
view.angle = angle;
view.ranking = thisview.ranking;
}
}
// need the distance for the (cx, cy) from the current north up position
var cx_offset = view.cx - m_center[0];
var cy_offset = view.cy - m_center[1];
var rotated_offset = pointsRotate([[cx_offset,cy_offset]],0,0,-view.angle);
map.flyTo({ bearing: view.angle, speed:0.00001 });
// once bearing is set, adjust to tightest fit
waitForMapMoveCompletion(function () {
var center2 = map.getCenter();
var m_center2 = ll2Mercator(center2.lng,center2.lat);
m_center2[0] += rotated_offset[0][0];
m_center2[1] += rotated_offset[0][1];
var ll_center2 = Mercator2ll(m_center2[0],m_center2[1]);
map.easeTo({
center:[ll_center2[0],ll_center2[1]],
zoom : map.getZoom() });
console.log("bearing:"+view.angle+ " scale:"+view.scale+" center: ("+ll_center2[0]+","+ll_center2[1]+")");
// draw the tight fitting trapezoid for reference purposes
var m_trapR = pointsRotate(m_trapezoid_path,m_center[0],m_center[1],-view.angle);
var m_trapRS = pointsScale(m_trapR,m_center[0],m_center[1],view.scale);
var m_trapRST = pointsTranslate(m_trapRS,m_center2[0]-m_center[0],m_center2[1]-m_center[1]);
convertMercator2LLAndDraw(m_trapRST,'#f0f',4);
});
}
function waitForMapMoveCompletion(func) {
if(map.isMoving())
setTimeout(function() { waitForMapMoveCompletion(func); },250);
else
func();
}
function thenSetPitch(path,pitch) {
map.flyTo({ pitch:pitch } );
waitForMapMoveCompletion(function() { thenCalculateOptimalView(path); })
}
function displayFittedView(path,pitch) {
var bbox = getBoundingBox(path);
var path_cx = (bbox[0][0]+bbox[1][0])/2;
var path_cy = (bbox[0][1]+bbox[1][1])/2;
// start with a 'north up' view
map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [path_cx, path_cy],
zoom: 12
});
// use the bounding box to get into the right zoom range
map.on('load', function () {
addLayerToMap("path",path,'#888',8);
map.fitBounds(bbox);
waitForMapMoveCompletion(function() { thenSetPitch(path,pitch); });
});
}
window.onload = function(e) {
displayFittedView(myPath,60);
}
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css' rel='stylesheet' />
<div id='map'></div>
Smallest surrounding rectangle would be specific to pitch=0 (looking directly down).
One option is to continue with smallest surrounding rectangle approach and calculate the transformation of the target area - just like a 3d engine does. If this is what you do maybe skim through unity docs to better understand the mechanics of viewing frustum
I feel this wouldn't be appropriate for your problem though as you'd have to re-calculate a 2d rendering of the target area from different angles, a relatively expensive brute force.
Another way to normalize the calculation would be to render a viewport projection into target area plane. See for yourself:
Then all you have to do is "just" figure out the largest size your original convex hull can fit into a trapezoid of that shape (specifically a convex isosceles trapezoid since we don't manipulate camera roll).
This is where I get a little out of depth and don't know where to point you for a calculation. I figure it's at least cheaper to iterate over possible solutions in this 2D space though.
P.S: One more thing to keep in mind is the viewport projection shape will be different depending on FOV (field of view).
This changes when you resize the browser viewport, but the property doesn't seem to be exposed in mapbox-gl-js.
Edit:
After some thought I feel the best mathematical solution can feel a little "dry" in reality. Not being across the use case and, possibly, making some wrong assumptions, I'd ask these questions:
For a route that's roughly a straight line, would it always be panned in so the ends are at bottom left and top right corners? That would be close to "optimal" but could get... boring.
Would you want to keep more of the path closer to the viewport? You can lose route detail if a large portion of it is far away from the viewport.
Would you pick points of interest to focus on? Those could be closer to the viewport.
Perhaps it would be handy to classify different types of routes by shape of hull and create panning presets?
Hopefully this can point you in the right direction with some tweaking.
First I set up the two points we want to show
let pointA = [-70, 43]
let pointB = [-83, 32]
Then I found the middle of those two points. I made my own function for this, but it looks like turf can do this.
function middleCoord(a, b){
let x = (a - b)/2
return _.min([a, b]) + x
}
let center = [middleCoord(pointA[0], pointB[0]), middleCoord(pointA[1], pointB[1])]
I used turfs bearing function to have the view from the 2nd point look at the first point
let p1 = turf.point(pointA)
let p2 = turf.point(pointB)
let points = turf.featureCollection([p1, p2])
let bearing = turf.bearing(p2, p1)
Then I call the map and run the fitBounds function:
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/outdoors-v10', //hosted style id
center: center, // starting position
zoom: 4, // starting zoom
pitch: 60,
bearing: bearing
})
map.fitBounds([pointA, pointB], {padding: 0, offset: 0})
Here's a codepen: https://codepen.io/thejoshderocher/pen/BRYGXq
To adjust the bearing to best use the screen size is to get the size of the window and adjust the bearing to take the most advantage of the available screen space. If it's a mobile screen in portrait, this bearing work perfect. If you are on a desktop with a wide view you will need to rotate so point A is in one of the top corners.

How to implement edge's on hover event on a canvas for arbor.js JavaScript library?

I'm trying to hack arbor.js so I can perform on edges hover event and then display any information from it.
So let's say I can move over any edge and detect which edge's been hovered.
This is the drawing edges part of the rendering code:
particleSystem.eachEdge(function(edge, pt1, pt2){
// edge: {source:Node, target:Node, length:#, data:{}}
// pt1: {x:#, y:#} source position in screen coords
// pt2: {x:#, y:#} target position in screen coords
// find the start point
var tail = intersect_line_box(pt1, pt2, nodeBoxes[edge.source.name])
var head = intersect_line_box(tail, pt2, nodeBoxes[edge.target.name])
ctx.save()
ctx.beginPath()
ctx.lineWidth = (!isNaN(weight)) ? parseFloat(weight) : 1
ctx.strokeStyle = (color) ? color : "#cccccc"
ctx.fillStyle = null
//We save edge's line data, "Diego"
var slope = ((head.y)-(tail.y))/((head.x)-(tail.x))
a.push({x1:tail.x, y1:tail.y, x2:head.x, y2:head.y, m:slope})
ctx.moveTo(tail.x, tail.y)
ctx.lineTo(head.x, head.y)
ctx.stroke()
// Why pushing here makes x2 and y2 undefined?, "Diego"
ctx.restore()
})
This is the refered function:
var intersect_line_box = function(p1, p2, boxTuple)
{
var p3 = {x:boxTuple[0], y:boxTuple[1]},
w = boxTuple[2],
h = boxTuple[3]
var tl = {x: p3.x, y: p3.y};
var tr = {x: p3.x + w, y: p3.y};
var bl = {x: p3.x, y: p3.y + h};
var br = {x: p3.x + w, y: p3.y + h};
return intersect_line_line(p1, p2, tl, tr) ||
intersect_line_line(p1, p2, tr, br) ||
intersect_line_line(p1, p2, br, bl) ||
intersect_line_line(p1, p2, bl, tl) ||
false
}
I thought about two approaches:
First approach
First one is adding a 'hover' anonymous function inside var handler = {...}. So when $(canvas).mousemove(handler.hover) all would be managed by hover's function. I thought capturing the mouse's position and comparing it with the previous edges line coordinates.
I have a variable like:
var a = []
And then, before drawing again I clear out saved edges:
a=[]
And save the to be drawed edges:
var slope = ((head.y)-(tail.y))/((head.x)-(tail.x))
a.push({x1:tail.x, y1:tail.y, x2:head.x, y2:head.y, m:slope})
So, hover's event function is:
hover: function(e) {
var pos = $(canvas).offset();
var x = e.pageX-pos.left
var y = e.pageY-pos.top
_mouseP = arbor.Point(e.pageX-pos.left, e.pageY-pos.top)
for (var i = 0; i<a.length; i++) {
if ( (_mouseP.x-a[i].x1)) == (a[i].m* (_mouseP.y-a[i].y1)) )
console.log("This is an edge)
},
Second approach
What about going for jQuery? So something like:
$("anyDrawedOnCanvasStuff").hover(function(){}
would trigger events automatically. However I don't think this as possible, is it?
I quite completed first approach, but something weird is happening with the saved points in a array, they're all double and obtained mouse points happened to be integer. And also, they don't even get close to complete the formula's logic.

Categories

Resources