How to create this Canvas Animation JS - javascript

Does someone know how is this animation build, which js framework is used or something that can help me get to know to recreate something similar?
Banner BG animation ---> https://envylabs.com/
Thanks in advance!

I can't tell with what library this animation was build with, because it is hidden in React bundled code, but I can show you a way to do something similar with Paper.js.
By looking at the animation, it seems that rules are:
a circle is influenced by mouse pointer if it is under a certain distance from it
the closest to the mouse pointer a circle is:
the bigger it becomes
the farther from the window center it goes.
Here is a Sketch implementing this.
//
// CONSTANTS
//
// user defined
var ROWS_COUNT = 10; // number of rows in the grid
var COLUMNS_COUNT = 10; // number of columns in the grid
var MOUSE_INFLUENCE_RADIUS = 350; // maximal distance from mouse pointer to be influenced
var INFLUENCE_SCALE_FACTOR = 1; // maximal influence on point scale
var INFLUENCE_POSITION_FACTOR = 15; // maximal influence on point position
// computed
var STEP_X = view.bounds.width / COLUMNS_COUNT;
var STEP_Y = view.bounds.height / ROWS_COUNT;
var RADIUS = Math.min(STEP_X, STEP_Y) * 0.1;
//
// ITEMS
//
// create a circle for each points in the grid
var circles = [];
for (var i = 0; i < COLUMNS_COUNT; i++)
{
for (var j = 0; j < COLUMNS_COUNT; j++)
{
var gridPoint = new Point((i + 0.5) * STEP_X, (j + 0.5) * STEP_Y);
circles.push(new Path.Circle({
center : gridPoint,
radius : RADIUS,
fillColor : 'black',
// matrix application is disabled in order to be able to manipulate scaling property
applyMatrix: false,
// store original center point as item custom data property
data : {gridPoint: gridPoint}
}));
}
}
//
// EVENTS
//
function onMouseMove(event)
{
for (var i = 0; i < circles.length; i++)
{
var circle = circles[ i ];
var gridPoint = circle.data.gridPoint;
var distance = event.point.getDistance(gridPoint);
// only influence circles that are in mouse influence zone
if (distance <= MOUSE_INFLUENCE_RADIUS)
{
var influence = 1 - distance / MOUSE_INFLUENCE_RADIUS;
// the closest the circle is from the mouse pointer
// the bigger it is
circle.scaling = 1 + influence * INFLUENCE_SCALE_FACTOR;
// the farthest it is from view center
circle.position = gridPoint + (gridPoint - view.center).normalize(influence * INFLUENCE_POSITION_FACTOR);
}
else
{
// reset circle state
circle.scaling = 1;
circle.position = gridPoint;
}
}
}

Your question is too open.
But there are some libraries that will save you a lot of time:
D3.js
Processing.js
Paper.js
There are a lot more, but it depends on what you need.

Related

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;

Maximum movement speed sprite PIXI.js

I'm writing a simple game in which a user can move around a sprite. By clicking the stage, the sprite moves towards that location. The problem I'm facing is that I want to set a speed for this sprite. I do not know the values the user is going to click. I can't think of a way in which the sprite's speed is always the same.
The thing with PIXI.js is that you can set the x and y movement speed of the sprite. I want the result of those movement speeds to always be the same, for example 5. So if the sprite moves down, the y-speed would be 5. When the sprite is moving diagonally, the diagonal speed should be 5. I currently use this script, but the solution I came up with does not completely work, as the speed differs for each time I click.
Does anyone have any idea how to solve this problem?
var Container = PIXI.Container,
autoDetectRenderer = PIXI.autoDetectRenderer,
loader = PIXI.loader,
resources = PIXI.loader.resources,
Sprite = PIXI.Sprite;
var stage = new PIXI.Container(),
renderer = PIXI.autoDetectRenderer(1000, 1000);
document.body.appendChild(renderer.view);
PIXI.loader
.add("rocket.png")
.load(setup);
var rocket, state;
function setup() {
//Create the `tileset` sprite from the texture
var texture = PIXI.utils.TextureCache["animal.png"];
//Create a rectangle object that defines the position and
//size of the sub-image you want to extract from the texture
var rectangle = new PIXI.Rectangle(192, 128, 32, 32);
//Tell the texture to use that rectangular section
texture.frame = rectangle;
//Create the sprite from the texture
rocket = new Sprite(texture);
rocket.anchor.x = 0.5;
rocket.anchor.y = 0.5;
rocket.x = 50;
rocket.y = 50;
rocket.vx = 0;
rocket.vy = 0;
//Add the rocket to the stage
stage.addChild(rocket);
document.addEventListener("click", function(){
x = event.clientX - rocket.x;
y = event.clientY - rocket.y;
rocket.vmax = 5;
var total = Math.abs(x) + Math.abs(y);
var tx = x/total;
var ty = y/total;
rocket.vx = tx*rocket.vmax;
rocket.vy = ty*rocket.vmax;
});
state = play;
gameLoop();
}
function gameLoop() {
//Loop this function at 60 frames per second
requestAnimationFrame(gameLoop);
state();
//Render the stage to see the animation
renderer.render(stage);
}
function play(){
rocket.x += rocket.vx;
rocket.y += rocket.vy;
}
How about this? This would normalize x and y.
var total = Math.Sqrt(x * x + y * y);
and it looks x and y are missing 'var'.
var x = event.clientX - rocket.x;
var y = event.clientY - rocket.y;

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.

Can I get help on a Rectilinear Polygon Hull Algorithm (in javascript if possible)

Edited title and here to reflect my question better.
My first description didn't get answers of the kind I was looking for and I didn't know there was a fancy name for this kind of shape I'm trying to make.
I studied Graham Scan algorithms but they don't completely cover the answer I'm looking for either, as they tend to "cut corners" if the shape becomes strange say.
Based on some articles I found, I'd started writing my own algorithm and it works a lot of the cases to be sure but I keep coming into problems I cant seem to completely stamp out, I believe the problem might be where I'm trying to calculate each of the corners.
This is the logic of my algorithm -
1st = Find the corners, top-left, top-right, bottom-right, bottom-left
2nd = Enter a while loop, travelling towards a target, starting from top-left travelling to top-right then to bottom right to bottom left and back to top left.
3rd = in each iteration of this while loop, we find the neighbours of each tile if they exist in this list, and choose the next tile to travel to, based on which comes first in an order decided by priority.
Priority explained, if the priority is to get to the top right corner, we take our time by first trying to travel to the left, then up, right and down but ONLY if a tile in this direction exists and ONLY if this tile isn't the previous tile we were standing at.
After we arrive at the top right corner, we shift the priority list making the first priority become the last. So
priority = [left, up, right, down]
becomes
priority = [up, right, down, left]
In the majority of cases my code IS performing as expected, like the logic seems sound, but there are a few anomaly moments where it doesn't seem to pick the right tile based on the priority I'm giving it and instead it wanders about the whole array. If anyone can help me where I'm making a mistake I'd appreciate this.
Heres my current algorithm code >
draw_hull = function() {
var list = [],
coordinates = [],
original_list = this.territory,
_x = 0, // coords are in arrays, [x,y] style.
_y = 1; // this is just for readers readability
// make list to be looped through, for each tile I deliberately added 4,
//it'll look more readable in the final product.
for (var i = 0; i < original_list.length; i++) {
list.push( [ (original_list[i][_x] * 32) + 8, (original_list[i][_y] * 32) + 8 ] );
list.push( [ (original_list[i][_x] * 32) + 24, (original_list[i][_y] * 32) + 8 ] );
list.push( [ (original_list[i][_x] * 32) + 24, (original_list[i][_y] * 32) + 24 ] );
list.push( [ (original_list[i][_x] * 32) + 8, (original_list[i][_y] * 32) + 24 ] );
}
// find corners
var topleft = 0, topright = 0, bottomright = 0, bottomleft = 0, hull = [];
for (var i = 0; i < list.length; i++) {
var x = list[i][_x];
var y = list[i][1];
if (x <= list[topleft][_x] && y <= list[topleft][_y]) topleft = i;
if (x >= list[topright][_x] && y <= list[topright][_y]) topright = i;
if (x >= list[bottomright][_x] && y >= list[bottomright][_y]) bottomright = i;
if (x <= list[bottomleft][_x] && y >= list[bottomleft][_y]) bottomleft = i;
}
// start drawing paths from one corner to the next, repeating until a full loop has been made
var priorities = ["l","u","r","d"], // travel the outline in this order, left up right down
current = topleft, // current tile
last = topleft; // last tile
target = [topright,bottomright,bottomleft,topleft],
target_iterator = 0, // target iterator so we know which corner we're striving towards
xx = 0, // an iterator to make sure this loop doesnt go on forever
done = false;
while(!done) {
// add the current tile to our hull list.
hull.push(current);
var next = {},
cx = list[current][_x],
cy = list[current][_y];
for (var i = 0; i < list.length; i++) {
var x = list[i][_x], y = list[i][_y];
if (x === cx && y === cy -16) { next.up = i; continue; }
if (x === cx && y === cy +16) { next.down = i; continue; }
if (x === cx -16 && y === cy) { next.left = i; continue; }
if (x === cx +16 && y === cy) { next.right = i; continue; }
}
var i = 0, check = priorities;
for (var i = 0; i < priorities.length; i++) {
var check = priorities[i];
// skip this if next check doesnt exist, or is the tile we just came from
if (next[check] == null || next[check] == undefined|| next[check] === last) continue;
var visited = false;
for (var j = 1; j < hull.length; j++) {
if (hull[j] === next[check]) {
visited = true;
continue;
}
}
if (visited) {
continue;
}
break;
}
last = current;
current = next[check];
xcoords = list[current][_x];
ycoords = list[current][_y];
coordinates.push([xcoords,ycoords]);
if (current === target[target_iterator]) {
priorities.push(priorities.shift());
target_iterator++;
if (target_iterator === 4) break;
}
xx++;
if (xx > 50) {break; debugger;}
}
if (xx < 50) this.hull = coordinates;
}
You should use moveTo() and lineTo() with a stroke(). Further documentation is available at MDN.
A simple drawing function could look like this: (or check Fiddle)
function drawShape(coords) {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.beginPath();
for(var i = 0; i < coords.length; i++) {
if(i === 0) {
ctx.moveTo(coords[i].x, coords[i].y);
} else {
ctx.lineTo(coords[i].x, coords[i].y);
}
}
ctx.closePath();
ctx.stroke();
}
You can easily stroke an "unlimited" amount of corners, or vertices of a shape using moveTo() and lineTo(). Below is an example of how you could draw a triangle, but you could easily extend it by adding more path methods.
var ctx = canvas.getContext('2d');
// Filled triangle
ctx.beginPath();
ctx.moveTo(25,25); //moves 'pen' to coords
ctx.lineTo(105,25); //draws line from prior coords to new specified coords.
ctx.lineTo(25,105);
ctx.fill(); //fills in shape
// Stroked triangle
ctx.beginPath();
ctx.moveTo(125,125);
ctx.lineTo(125,45);
ctx.lineTo(45,125);
ctx.closePath();
ctx.stroke(); //strokes along path (i.e shape outline)
<canvas id="canvas" width="200" height="200"></canvas>
This code can be found at https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes along with detailed advice on how to draw more complex shapes.
Ok, further studies, I found a name for the type of shape I'm trying to draw.
Its called an Rectilinear polygon.
Wikipedia's article
Stack Overflow already has an answer for this, was just hard to find.

Javascript Julia Fractal slow and not detailed

I am trying to generate a Julia fractal in a canvas in javascript using math.js
Unfortunately every time the fractal is drawn on the canvas, it is rather slow and not very detailed.
Can anyone tell me if there is a specific reason this script is so slow or is it just to much to ask of a browser? (note: the mouse move part is disabled and it is still kinda slow)
I have tried raising and lowering the “bail_num” but everything above 1 makes the browser crash and everything below 0.2 makes everything black.
// Get the canvas and context
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
// Width and height of the image
var imagew = canvas.width;
var imageh = canvas.height;
// Image Data (RGBA)
var imagedata = context.createImageData(imagew, imageh);
// Pan and zoom parameters
var offsetx = -imagew/2;
var offsety = -imageh/2;
var panx = -2000;
var pany = -1000;
var zoom = 12000;
// c complexnumber
var c = math.complex(-0.310, 0.353);
// Palette array of 256 colors
var palette = [];
// The maximum number of iterations per pixel
var maxiterations = 200;
var bail_num = 1;
// Initialize the game
function init() {
//onmousemove listener
canvas.addEventListener('mousemove', onmousemove);
// Generate image
generateImage();
// Enter main loop
main(0);
}
// Main loop
function main(tframe) {
// Request animation frames
window.requestAnimationFrame(main);
// Draw the generate image
context.putImageData(imagedata, 0, 0);
}
// Generate the fractal image
function generateImage() {
// Iterate over the pixels
for (var y=0; y<imageh; y++) {
for (var x=0; x<imagew; x++) {
iterate(x, y, maxiterations);
}
}
}
// Calculate the color of a specific pixel
function iterate(x, y, maxiterations) {
// Convert the screen coordinate to a fractal coordinate
var x0 = (x + offsetx + panx) / zoom;
var y0 = (y + offsety + pany) / zoom;
var cn = math.complex(x0, y0);
// Iterate
var iterations = 0;
while (iterations < maxiterations && math.norm(math.complex(cn))< bail_num ) {
cn = math.add( math.sqrt(cn) , c);
iterations++;
}
// Get color based on the number of iterations
var color;
if (iterations == maxiterations) {
color = { r:0, g:0, b:0}; // Black
} else {
var index = Math.floor((iterations / (maxiterations)) * 255);
color = index;
}
// Apply the color
var pixelindex = (y * imagew + x) * 4;
imagedata.data[pixelindex] = color;
imagedata.data[pixelindex+1] = color;
imagedata.data[pixelindex+2] = color;
imagedata.data[pixelindex+3] = 255;
}
function onmousemove(e){
var pos = getMousePos(canvas, e);
//c = math.complex(-0.3+pos.x/imagew, 0.413-pos.y/imageh);
//console.log( 'Mouse position: ' + pos.x/imagew + ',' + pos.y/imageh );
// Generate a new image
generateImage();
}
function getMousePos(canvas, e) {
var rect = canvas.getBoundingClientRect();
return {
x: Math.round((e.clientX - rect.left)/(rect.right - rect.left)*canvas.width),
y: Math.round((e.clientY - rect.top)/(rect.bottom - rect.top)*canvas.height)
};
}
init();
The part of the code that is executed most is this piece:
while (iterations < maxiterations && math.norm(math.complex(cn))< bail_num ) {
cn = math.add( math.sqrt(cn) , c);
iterations++;
}
For the given canvas size and offsets you use, the above while body is executed 19,575,194 times. Therefore there are some obvious ways to improve performance:
somehow reduce the number of points for which the loop must be executed
somehow reduce the number of times these statements are executed per point
somehow improve these statements so they execute faster
The first idea is easy: reduce the canvas dimensions. But this is maybe not something you'd like to do.
The second idea can be achieved by reducing the value for bail_num, because then the while condition will be violated sooner (given that the norm of a complex number is always a positive real number). However, this will just result in more blackness, and gives the same visual effect as zooming out of the center of the fractal. Try for instance with 0.225: there just remains a "distant star". When bail_num is reduced too much, you wont even find the fractal anymore, as everything turns black. So to compensate you would then probably want to change your offset and zoom factors to get a closer view at the center of the fractal (which is still there, BTW!). But towards the center of the fractal, points need more iterations to get below bail_num, so in the end nothing is gained: you'll be back at square one with this method. It's not really a solution.
Another way to work along the second idea is to reduce maxiterations. However, this will reduce the resolution accordingly. It is clear that you will have fewer colors at your disposal, as this number directly corresponds to the number of iterations you can have at the most.
The third idea means that you would somehow optimise the calculations with complex numbers. It turns out to give a lot of gain:
Use efficient calculations
The norm that is calculated in the while condition could be used as an intermediate value for calculating the square root of the same number, which is needed in the next statement. This is the formula for getting the square root from a complex number, if you already have its norm:
__________________
root.re = √ ½(cn.re + norm)
root.im = ½cn.im/root.re
Where the re and im properties denote the real and imaginary components of the respective complex numbers. You can find the background for these formulas in this answer on math.stackexchange.
As in your code the square root is calculated separately, without taking benefit of the previous calculation of the norm, this will certainly bring a benefit.
Also, in the while condition you don't really need the norm (which involves a square root) for comparing with bail_num. You could omit the square root operation and compare with the square of bail_num, which comes down to the same thing. Obviously you would have to calculate the square of bail_num only once at the start of your code. This way you can delay that square root operation for when the condition is found true. The formula for calculating the square of the norm is as follows:
square_norm = cn.re² + cn.im²
The calls of methods on the math object have some overhead, since this library allows different types of arguments in several of its methods. So it would help performance if you would code the calculations directly without relying on math.js. The above improvements already started doing that anyway. In my attempts this also resulted in a considerable gain in performance.
Predefine colours
Although not related to the costly while loop, you can probably gain a litte bit more by calculating all possible colors (per number of iterations) at the start of the code, and store them in an array keyed by number of iterations. That way you can just perform a look-up during the actual calculations.
Some other similar things can be done to save on calculations: For instance, you could avoid translating the screen y coordinate to world coordinates while moving along the X axis, as it will always be the same value.
Here is the code that reduced the original time to complete by a factor of 10, on my PC:
Added intialisation:
// Pre-calculate the square of bail_num:
var bail_num_square = bail_num*bail_num;
// Pre-calculate the colors:
colors = [];
for (var iterations = 0; iterations <= maxiterations; iterations++) {
// Note that I have stored colours in the opposite direction to
// allow for a more efficient "countdown" loop later
colors[iterations] = 255 - Math.floor((iterations / maxiterations) * 255);
}
// Instead of using math for initialising c:
var cx = -0.310;
var cy = 0.353;
Replace functions generateImage and iterate by this one function
// Generate the fractal image
function generateImage() {
// Iterate over the pixels
var pixelindex = 0,
step = 1/zoom,
worldX, worldY,
sq, rootX, rootY, x0, y0;
for (var y=0; y<imageh; y++) {
worldY = (y + offsety + pany)/zoom;
worldX = (offsetx + panx)/zoom;
for (var x=0; x<imagew; x++) {
x0 = worldX;
y0 = worldY;
// For this point: iterate to determine color index
for (var iterations = maxiterations; iterations && (sq = (x0*x0+y0*y0)) < bail_num_square; iterations-- ) {
// root of complex number
rootX = Math.sqrt((x0 + Math.sqrt(sq))/2);
rootY = y0/(2*rootX);
x0 = rootX + cx;
y0 = rootY + cy;
}
// Apply the color
imagedata.data[pixelindex++] =
imagedata.data[pixelindex++] =
imagedata.data[pixelindex++] = colors[iterations];
imagedata.data[pixelindex++] = 255;
worldX += step;
}
}
}
With the above code you don't need to include math.js anymore.
Here is a smaller sized snippet with mouse events handled:
// Get the canvas and context
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
// Width and height of the image
var imagew = canvas.width;
var imageh = canvas.height;
// Image Data (RGBA)
var imagedata = context.createImageData(imagew, imageh);
// Pan and zoom parameters
var offsetx = -512
var offsety = -430;
var panx = -2000;
var pany = -1000;
var zoom = 12000;
// Palette array of 256 colors
var palette = [];
// The maximum number of iterations per pixel
var maxiterations = 200;
var bail_num = 0.8; //0.225; //1.15;//0.25;
// Pre-calculate the square of bail_num:
var bail_num_square = bail_num*bail_num;
// Pre-calculate the colors:
colors = [];
for (var iterations = 0; iterations <= maxiterations; iterations++) {
colors[iterations] = 255 - Math.floor((iterations / maxiterations) * 255);
}
// Instead of using math for initialising c:
var cx = -0.310;
var cy = 0.353;
// Initialize the game
function init() {
// onmousemove listener
canvas.addEventListener('mousemove', onmousemove);
// Generate image
generateImage();
// Enter main loop
main(0);
}
// Main loop
function main(tframe) {
// Request animation frames
window.requestAnimationFrame(main);
// Draw the generate image
context.putImageData(imagedata, 0, 0);
}
// Generate the fractal image
function generateImage() {
// Iterate over the pixels
console.log('generate', cx, cy);
var pixelindex = 0,
step = 1/zoom,
worldX, worldY,
sq_norm, rootX, rootY, x0, y0;
for (var y=0; y<imageh; y++) {
worldY = (y + offsety + pany)/zoom;
worldX = (offsetx + panx)/zoom;
for (var x=0; x<imagew; x++) {
x0 = worldX;
y0 = worldY;
// For this point: iterate to determine color index
for (var iterations = maxiterations; iterations && (sq_norm = (x0*x0+y0*y0)) < bail_num_square; iterations-- ) {
// root of complex number
rootX = Math.sqrt((x0 + Math.sqrt(sq_norm))/2);
rootY = y0/(2*rootX);
x0 = rootX + cx;
y0 = rootY + cy;
}
// Apply the color
imagedata.data[pixelindex++] =
imagedata.data[pixelindex++] =
imagedata.data[pixelindex++] = colors[iterations];
imagedata.data[pixelindex++] = 255;
worldX += step;
}
}
console.log(pixelindex);
}
function onmousemove(e){
var pos = getMousePos(canvas, e);
cx = -0.31+pos.x/imagew/150;
cy = 0.35-pos.y/imageh/30;
generateImage();
}
function getMousePos(canvas, e) {
var rect = canvas.getBoundingClientRect();
return {
x: Math.round((e.clientX - rect.left)/(rect.right - rect.left)*canvas.width),
y: Math.round((e.clientY - rect.top)/(rect.bottom - rect.top)*canvas.height)
};
}
init();
<canvas id="myCanvas" width="512" height="200"></canvas>

Categories

Resources