I am using leaflet with openstreetmap to create a fixed grid on top of the world map that consists of 100m x 100m tiles. Basically, I am creating a turn-based game, where a player should be able to click on a certain tile, which then reveals a context menu. The server is going to know that the player has opened the tile for a certain place.
I tried the following:
<!DOCTYPE html>
<html>
<head>
<title>GridLayer Test</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.0.1/dist/leaflet.css" />
<style>
body {
padding: 0;
margin: 0;
}
html,
body,
#map {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet#1.0.1/dist/leaflet.js"></script>
<script>
var map = new L.Map('map', { center: [10, 0], zoom: 2 });
var tiles = new L.GridLayer();
tiles.createTile = function (coords) {
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
var ctx = tile.getContext('2d');
var size = this.getTileSize()
tile.width = size.x
tile.height = size.y
// calculate projection coordinates of top left tile pixel
var nwPoint = coords.scaleBy(size)
// calculate geographic coordinates of top left tile pixel
var nw = map.unproject(nwPoint, coords.z)
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, size.x, 50);
ctx.fillStyle = 'black';
ctx.fillText('x: ' + coords.x + ', y: ' + coords.y + ', zoom: ' + coords.z, 20, 20);
ctx.fillText('lat: ' + nw.lat + ', lon: ' + nw.lng, 20, 40);
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(size.x - 1, 0);
ctx.lineTo(size.x - 1, size.y - 1);
ctx.lineTo(0, size.y - 1);
ctx.closePath();
ctx.stroke();
return tile;
}
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap',
minNativeZoom: 1,
maxNativeZoom: 1,
}).addTo(map)
tiles.addTo(map)
</script>
</body>
</html>
As you can see the grid changed when I zoom in or out, even though I used minNativeZoom. However, I would like to have the grid fixed and 100m x 100m wide.
I also tried to only return tile when zoomLevel = 18. This does not work.
Any suggestions what I am doing wrong?
I appreciate your replies!
You can draw a grid with the following createTile implementation:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GridLayer Test</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.0.1/dist/leaflet.css" />
<style>
body {
padding: 0;
margin: 0;
}
html,
body,
#map {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet#1.0.1/dist/leaflet.js"></script>
<script>
const numTilesX = 2 ** 17
const numTilesY = 2 ** 17
class TileNumber {
constructor(x, y) {
this.x = x;
this.y = y;
}
equals(other) {
return this.x === other.x && this.y === other.y;
}
}
let coloredTiles = [
new TileNumber(70435, 45249),
new TileNumber(70434, 45248),
new TileNumber(70441, 45245)
]
function latLngToTileNumber(latLng) {
const lngDegrees = latLng.lng;
const latRadians = latLng.lat * (Math.PI/180);
return new L.Point(
numTilesX * ((lngDegrees + 180) / 360),
numTilesY * (1 - Math.log(Math.tan(latRadians) + 1 / Math.cos(latRadians)) / Math.PI) / 2
);
}
const map = new L.Map('map', {center: [48.5748229, 13.4609744], zoom: 16, maxZoom: 19});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap', maxZoom: 19
}).addTo(map)
const tiles = new L.GridLayer({minZoom: 12});
tiles.createTile = function (coords) {
const tile = L.DomUtil.create('canvas', 'leaflet-tile');
const ctx = tile.getContext('2d');
const size = this.getTileSize();
tile.width = size.x
tile.height = size.y
// calculate projection coordinates of top left tile pixel
const nwPoint = coords.scaleBy(size);
// calculate geographic coordinates of top left tile pixel
const nw = map.unproject(nwPoint, coords.z);
// calculate fraction tile number at top left point
const nwTile = latLngToTileNumber(nw, Math.floor)
// calculate projection coordinates of bottom right tile pixel
const sePoint = new L.Point(nwPoint.x + size.x - 1, nwPoint.y + size.y - 1)
// calculate geographic coordinates of bottom right tile pixel
const se = map.unproject(sePoint, coords.z);
// calculate fractional tile number at bottom right point
const seTile = latLngToTileNumber(se, Math.ceil)
const minTileX = nwTile.x
const maxTileX = seTile.x
const minTileY = nwTile.y
const maxTileY = seTile.y
for (let x = Math.ceil(minTileX) - 1; x <= Math.floor(maxTileX) + 1; x++) {
for (let y = Math.ceil(minTileY) - 1; y <= Math.floor(maxTileY) + 1; y++) {
let tile = new TileNumber(x, y)
const xMinPixel = Math.round(size.x * (x - minTileX) / (maxTileX - minTileX));
const xMaxPixel = Math.round(size.x * (x + 1 - minTileX) / (maxTileX - minTileX));
const yMinPixel = Math.round(size.y * (y - minTileY) / (maxTileY - minTileY));
const yMaxPixel = Math.round(size.y * (y + 1 - minTileY) / (maxTileY - minTileY));
// fill the rectangle with a color
ctx.fillStyle = coloredTiles.some(t => t.equals(tile))
? 'rgba(0, 0, 255, 0.3)'
: 'rgba(255, 255, 255, 0)';
ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel);
if (coords.z >= 16) {
// draw the white rectangle and text at the top of the cell
ctx.fillStyle = 'white';
ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, 28);
ctx.fillStyle = 'black';
ctx.font = "15px Arial"
ctx.fillText(tile.x + "," + tile.y, xMinPixel + 10, yMinPixel + 20, xMaxPixel - xMinPixel);
}
if (coords.z >= 13) {
// draw a border
ctx.strokeStyle = 'black';
ctx.strokeRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel);
}
}
}
return tile;
}
tiles.addTo(map);
map.on('click', e => {
const fractionalTileNumber = latLngToTileNumber(e.latlng);
const tileNumber = new TileNumber(Math.floor(fractionalTileNumber.x), Math.floor(fractionalTileNumber.y));
console.log("Tile " + tileNumber.x + " " + tileNumber.y + " clicked");
if (coloredTiles.some(t => t.equals(tileNumber))) {
coloredTiles = coloredTiles.filter(t => !t.equals(tileNumber));
} else {
coloredTiles.push(tileNumber);
}
tiles.redraw();
});
</script>
</body>
</html>
Some caveats:
Because the earth isn't flat, it's not really possible to cleanly cover it with a grid of rectangles. So I did the closest thing to it by drawing grid boundaries along latitude and longitude lines. As a result, the tiles will get larger (cover more square meters) towards the equator and smaller towards the poles.
Each grid cell has an unique TileNumber (x and y coordinate, starting at 0,0 in the northwestern corner).
To demonstrate clicking on the map, I'm writing the TileNumber of the to the log and toggle the tile's colored/uncolored state. Of course, this could be replaced with any other functionality imaginable, including communication with a server.
As this snippet includes the ability to color the grid cells based on their TileNumber (stored in an array), I've provided this as an answer to your question about coloring grid tiles as well.
Related
I'm working on a fun little simulation environment for circles. I cannot find an accurate way to combine two circles and find their center coordinate.
I set up an html canvas, then generate random coords on the plane along with a random sized radius. After every generation, I check for an intersection between every circle and every other circle. When circles intersect I want them to merge - making a circle with the combined surface area. Finding the coordinates of the new center is my issue.
I don't want to simply find the midpoint of the centers because that doesn't factor in the size of the circles. A humongous circle could be swayed by a tiny one, which doesn't make for a realistic simulation.
I've thought up what I think is a bad solution: multiplying the change in distance created by the midpoint formula by the ratio of the two circles radii, getting the angle of the resulting triangle, using trig to get the x and y difference, then adding that to the center of the larger circle and calling it a day.
Really have no clue if that is the right way to do it, so I wanted to ask people smarter than me.
Oh also here's a link to the repo on github:
Circle Simulator
This is my first stackOverflow question so bear with me if I did something completely stupid. Thanks everyone!
var dataForm = document.getElementById('dataForm');
var type = document.getElementById('type');
var dataMinRad = document.getElementById('dataMinRad');
var dataMaxRad = document.getElementById('dataMaxRad');
var phaseInterval = document.getElementById('phaseInterval');
//form on submit
const onDataSubmit = (e) => {
if (e) e.preventDefault();
//updates min and max radius
minRadius = parseInt(dataMinRad.value);
maxRadius = parseInt(dataMaxRad.value);
//clears canvas
c.clearRect(0, 0, canvas.width, canvas.height);
//clears circles
circles = [];
//clears any previous interval
clearInterval(phase);
let generator = eval(type.value), data;
//every one second this code is repeated
phase = setInterval(() => {
//gets the circle data from whatever generator is selected
data = generator();
//adds the new circle and draws it on the canvas if the data is good
if (data) {
circles.push(new Circle(data.x, data.y, data.rad));
circles[circles.length - 1].draw();
}
}, parseInt(phaseInterval.value));
}
dataForm.addEventListener('submit', onDataSubmit);
</script>
<script>
//initializes global elements
var stage = document.getElementById('stage');
var canvas = document.getElementById('myCanvas');
var c = canvas.getContext('2d');
//sets width and height of canvas to that of the stage
canvas.setAttribute('width', stage.clientWidth);
canvas.setAttribute('height', stage.clientHeight);
class Circle {
constructor (x, y, rad) {
this.x = x;
this.y = y;
this.rad = rad;
}
draw() {
c.fillStyle = 'black';
c.beginPath();
c.arc(this.x, this.y, this.rad, 0, 2 * Math.PI, true);
c.stroke();
}
}
//variables
var circles = [];
var maxRadius = 100;
var minRadius = 1;
var phase;
const random = () => {
//random coords and radius
let x, y, rad;
do {
[x, y, rad] = [Math.round(Math.random() * canvas.width), Math.round(Math.random() * canvas.height), Math.ceil(Math.random() * (maxRadius - minRadius)) + minRadius];
} while ((() => {
for (let i in circles) {
if (Math.sqrt(Math.pow(x - circles[i].x, 2) + Math.pow(y - circles[i].y, 2)) < rad + circles[i].rad) {
return true;
}
}
return false;
})()) //end while
return { x: x, y: y, rad: rad};
}
const order = () => {
//gets some random coords and sets the radius to max
let [x, y, rad] = [Math.round(Math.random() * canvas.width), Math.round(Math.random() * canvas.height), maxRadius];
//decreases the radius while the resulting circle still intercects any other circle
while (rad >= minRadius && (() => {
for (let i in circles) {
if (Math.sqrt(Math.pow(x - circles[i].x, 2) + Math.pow(y - circles[i].y, 2)) < rad + circles[i].rad) {
return true;
}
}
return false;
})()) {
rad--;
}
//only sends the radii that are greater than the minimum radius
if (rad >= minRadius) return { x: x, y: y, rad: rad};
}
//the position changes must be weighted somehow
const agar = () => {
//some looping control variables
let i = 0, j = 1, noChange = true;
//loops through the circles array in every circle until the noChange variable is false
while (i < circles.length && noChange) {
while (j < circles.length && noChange) {
//checks if each circle is inside each other circle
if (Math.sqrt(Math.pow(circles[i].x - circles[j].x, 2) + Math.pow(circles[i].y - circles[j].y, 2)) < circles[i].rad + circles[j].rad) {
//copies the two circles
let tempCircles = [circles[i], circles[j]];
//splices the item closest to the end of the array first so that the position of the other doesn't shift after the splice
if (i > j) {
circles.splice(i, 1);
circles.splice(j, 1);
} else {
circles.splice(j, 1);
circles.splice(i, 1);
}
//radius of the two circles' surface area combined
let rad = Math.sqrt(tempCircles[0].rad * tempCircles[0].rad + tempCircles[1].rad * tempCircles[1].rad);
/*
// method 1: the midpoint of the centers //
let x = (tempCircles[0].x + tempCircles[1].x) / 2;
let y = (tempCircles[0].y + tempCircles[1].y) / 2;
*/
// method 2: the radius ratio weighted //
let bigCircle, smallCircle;
if (tempCircles[0].rad > tempCircles[1].rad) {
bigCircle = tempCircles[0];
smallCircle = tempCircles[1];
} else {
bigCircle = tempCircles[1];
smallCircle = tempCircles[0];
}
//get the distance between the two circles
let dist = Math.sqrt(Math.pow(bigCircle.x - smallCircle.x, 2) + Math.pow(bigCircle.y - smallCircle.y, 2));
//gets the ratio of the two circles radius size
let radRatio = smallCircle.rad / bigCircle.rad;
//the adjusted hypot for the ratio
dist = dist * radRatio;
//the angle
let theta = Math.atan2(smallCircle.y - bigCircle.y, smallCircle.x - bigCircle.x); // all hail atan2!
//the new center coords
let x = bigCircle.x + dist * Math.cos(theta);
let y = bigCircle.y + dist * Math.sin(theta);
circles.push(new Circle(x, y, rad));
//change happened so the variable should be false
noChange = false;
/*
-find the middle of the point
-weigh it in the direction of teh biggest circle
radius as the magnitude and [angle of the triangle created when the centers are connected] as the direction for both radii.
find the point on each circle closest to the center of the other circle
find those two points midpoint
find the distance from that point to each of the centers
those two distances are the magnitude of two new vectors with the same angels as before
add those two vectors
is there really not a freaking easier way?
*/
/*
try this:
-get the distance between the centers.
-multiply that by the ratio
-get the angle
-use that angle and that hypot to find the x and y
-add the x and y to the bigger circles centerr
*/
}
j++;
}
i++;
j = i + 1;
}
//if there was no change
if (noChange) {
//random coords and radius size
let x = Math.round(Math.random() * canvas.width),
y = Math.round(Math.random() * canvas.height),
rad = Math.ceil(Math.random() * (maxRadius - minRadius)) + minRadius;
//adds the random circle to the array
circles.push(new Circle(x, y, rad));
}
//clears canvas
c.clearRect(0, 0, canvas.width, canvas.height);
//redraws ALL circles
for (let i in circles) {
circles[i].draw();
}
}
onDataSubmit();
* {
margin: 0;
box-sizing: border-box;
}
#wrapper {
width: 100%;
max-width: 1280px;
margin: auto;
margin-right: 0;
display: flex;
flex-flow: row nowrap;
}
#dataContainer {
height: 100%;
width: 20%;
padding: 5px;
}
#dataContainer>* {
padding: 15px;
}
#dataForm {
max-width: 200px;
display: grid;
}
#dataForm>* {
margin-top: 5px;
width: 100%;
}
.center {
margin: auto;
}
#stage {
margin: 5px;
width: 80%;
height: 97vh;
}
<div id='wrapper'>
<!-- form containter -->
<div id='dataContainer'>
<h3>Data</h3>
<form id='dataForm' method='post'>
<label for='type'>Type:</label>
<select id='type' name='type'>
<option value='random' selected>Random</option>
<option value='order'>Order</option>
<option value='agar'>Agario</option>
</select>
<label for='min'>Min-Radius:</label>
<input id='dataMinRad' name='min' type='number' value='1' min='0'>
<label for='max'>Max-Radius:</label>
<input id='dataMaxRad' name='max' type='number' value='100'>
<label for='interval'>Phase Interval:</label>
<input id='phaseInterval' name='interval' type='number' value='1' min='1'>
<button type='submit' id='dataSubmit' class='center'>Load</submit>
</form>
</div>
<!-- canvas container-->
<div id='stage'>
<canvas id='myCanvas'></canvas>
</div>
</div>
So the question is given two overlapping circles find a new circle that represents the merged circles and has an area equal to the sum of the original circles.
For the center of this new circle, one choice is to find the center of mass of the two original circles. If you have two masses of masses m1, m2 and positions (x1,y1), (x2,y2) then the center of mass of the whole system would be
m1/(m1+m2) (x1,y1) + m2/(m1+m2) (x2,y2)
In this case, the center of mass will be
r1^2/(r1^2+r2^2) (x1,y1) + r2^2/(r1^2+r2^2) (x2,y2)
For circles with radii r1, r2 then the masses will be proportional to pi r1^2 + pi r2^2. Hence, the radius of the circle will be sqrt(r1^2+r2^2).
If you can get the center point coordinates and radius, you can draw this new circle
like this:
And about extract a root √ you can use this:
var xxxx = Math.pow(your target here,2);
Update my answer:
Wasted weeks - Need solution to edit Polygons using either Fabric.js and Konva.js - Both have no way to actually update the poly points and transformer when the poly or it's points are MOVED, FLIPPED or MIRRORED. I'll assume the array points need to be reversed and the end the starting index switch depending on the quadrant the poly has been flipped.
If anyone have a solution please post. Fabric.js code in CodePen: https://codepen.io/Rstewart/pen/LYbJwQE
/* CODE FROM POLY DEMO ON FABRIC WEBSITE - CODE FAILS WHEN FLIPPED OR MIRRORED */
function polygonPositionHandler(dim, finalMatrix, fabricObject) {
var x = (fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x),
y = (fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y);
return fabric.util.transformPoint( { x: x, y: y },
fabric.util.multiplyTransformMatrices(
fabricObject.canvas.viewportTransform,
fabricObject.calcTransformMatrix()
)
);
}
function actionHandler(eventData, transform, x, y) {
var polygon = transform.target, currentControl = polygon.controls[polygon.__corner],
mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center'),
polygonBaseSize = polygon._getNonTransformedDimensions(), size = polygon._getTransformedDimensions(0, 0),
finalPointPosition = {
x: mouseLocalPosition.x * polygonBaseSize.x / size.x + polygon.pathOffset.x,
y: mouseLocalPosition.y * polygonBaseSize.y / size.y + polygon.pathOffset.y
};
polygon.points[currentControl.pointIndex] = finalPointPosition; return true;
}
function anchorWrapper(anchorIndex, fn) {
return function(eventData, transform, x, y) {
var fabricObject = transform.target,
absolutePoint = fabric.util.transformPoint({
x: (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x),
y: (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y),
}, fabricObject.calcTransformMatrix()),
actionPerformed = fn(eventData, transform, x, y),
newDim = fabricObject._setPositionDimensions({}),
polygonBaseSize = fabricObject._getNonTransformedDimensions(),
newX = (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x,
newY = (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y;
fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
return actionPerformed;
}
}
I can't work out why my sprites are not working in matter.js. I am creating a car/football game and have disabled wireframes on the renderer but the sprites are still not applying correctly.
My first issue is that when I apply a sprite texture to a composite body (the car), the sprite does not render at all.
My second issue is that when I apply a sprite texture to the body of the car, the sprite does not rotate with the body (the sprite does not rotate at all).
My third issue is that when I apply a sprite texture to the ball (not a composite body), both the sprite and the body representing the ball become invisible. They ball is still visible to the engine but neither the body or the sprite can be seen on the canvas.
function game ()
{
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// module aliases
var Engine = Matter.Engine,
Render = Matter.Render,
World = Matter.World,
Bodies = Matter.Bodies,
Body = Matter.Body,
Composite = Matter.Composite,
Composites = Matter.Composites,
Vertices = Matter.Vertices;
// create an engine
var engine = Engine.create();
engine.world.gravity.y = 0.6;
// create a renderer
var render = Render.create
({
//element: canvas,
element: document.body,
canvas: canvas,
engine: engine,
options:
{
width: window.innerWidth - 30,
height: window.innerHeight - 30,
wireframes: false
}
});
var offset = 1;
var wallSize = 20;
var ground = Bodies.rectangle(400, 510, 1810, 60,
{
isStatic: true,
friction: 0,
restitution: 0
});
var ball = Bodies.circle(window.innerWidth/2, window.innerHeight/2, 40,
{
mass: 5,// Used to be 0.5
restitution: 0.95,
friction: 0,
frictionAir: 0.01,
});
ball.render.sprite.texture = "soccarball.png";
const carBody = Matter.Bodies.fromVertices(100, 100, [{x:200, y:200},{x:260, y:210},{x:260, y:220},{x: 200, y: 220}]);
carBody.render.sprite.texture = "car_sprites.jpg";
carBody.render.sprite.xScale = 0.06;
carBody.render.sprite.yScale = 0.06;
const frontWheel = Matter.Bodies.circle(100 -20, 115, 8);
const rearWheel = Matter.Bodies.circle(100 +20, 115, 8);
const car = Body.create
({
parts: [carBody, frontWheel, rearWheel],
inertia: 100000,
friction: 0,
mass: 100,
restitution: -1,
});
var floor = Bodies.rectangle(window.innerWidth/2, window.innerHeight + offset, window.innerWidth + 2 * offset, wallSize,
{
isStatic: true, friction: 0
});
World.add(engine.world, [ground, car, ball, floor]);
// MAIN lOOP
function cycle()
{
requestAnimationFrame(cycle);
}
cycle();
// run the engine
Engine.run(engine);
//Engine.update(engine);
// run the renderer
Render.run(render);
}
window.onload = game();
///////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
function checkButtons()
{
if(keys["68"]) // KEY_D
{
if(car.speed < 10)
{
//Body.applyForce( car, {x: car.position.x, y: car.position.y}, {x: 0.5, y: 0})
let force = (0.001 * car.mass);
Body.applyForce(car,car.position,{x:force,y:0});
//console.log("Car Speed: " + car.speed);
}
}
if(keys["87"]) // KEY_W
{
if(onGround())
{
carAvailableJumps--;
let verticalForce = (-0.013 * car.mass);
Body.applyForce(car,car.position,{x:0,y:verticalForce});
}
if(carAvailableJumps > 0)
{
if(!onGround() && keys["68"]) // KEY_D
{
carAvailableJumps--;
let rotationalForce = (0.0009 * car.mass);
Body.applyForce(car,{x: (car.position.x - carWidth/2), y: (car.position.y)},{x:0,y:-rotationalForce});
}
if(!onGround() && keys["65"]) // KEY_A
{
carAvailableJumps--;
let rotationalForce = (0.0009 * car.mass);
Body.applyForce(car,{x: (car.position.x - carWidth/2), y: (car.position.y)},{x:0,y:rotationalForce});
}
}
}
if(keys["83"]) // KEY_S
{
}
if(keys["65"]) // KEY_A
{
if(car.speed < 10)
{
//Body.applyForce( car, {x: car.position.x, y: car.position.y}, {x: 0.5, y: 0})
let force = (-0.001 * car.mass);
Body.applyForce(car,car.position,{x:force,y:0});
//console.log("Car Speed: " + car.speed);
}
}
}
*/
<!DOCTYPE HTML>
<html>
<meta charset="UTF-8"/>
<head>
<title>This is the title</title>
</head>
<body>
<div id="div">
<canvas id="myCanvas"</canvas>
<script src="matter.js" type="text/javascript"></script>
<script src="internethelp.js" type="text/javascript"></script>
</div>
</body>
</html>
You are supposed to draw the car and other sprites yourself on the canvas. Matter only calculates the coordinates in the physics world for you. You have to apply those coordinates to the objects that you draw in your canvas.
This is an example where the matter coordinates are read, and then used to position a DIV element in the DOM
let physicsBox = Matter.Bodies.rectangle(x, y, w, h);
Matter.World.add(world, [physicsBox]);
let div = document.createElement("box");
document.body.appendChild(div);
// draw
let pos = physicsBox.position;
let angle = physicsBox.angle;
let degrees = angle * (180 / Math.PI);
div.style.transform = "translate(" + (pos.x - 10) + "px, " + (pos.y - 10) + "px) rotate(" + degrees + "deg)";
Let's say I need to put a text in the middle of the area of a triangle.
I can calculate the coordinates of the triangle's center using getBBox():
var triangle = "M0,0 L100,0 100,50 z";
var BBox = paper.path(triangle).getBBox();
var middle;
middle.x = BBox.x + BBox.width/2;
middle.y = BBox.y + BBox.height/2;
This results in the coordinates (50, 25) which are always on the long side of the triangle.
How can I make sure the calculated "middle" is inside the triangle?
The correct coordinates should be approximately: (75, 25).
The code should of course be independent of this particular example, it should work for any kind of shape.
I've done some more research in the topic, and following an advice from another list I got here:
https://en.wikipedia.org/wiki/Centroid
There is an algorithm there to calculate the centroid of an irregular polygon, which I have translated into this code:
function getCentroid(path) {
var x = new Array(11);
var y = new Array(11);
var asum = 0, cxsum = 0, cysum = 0;
var totlength = path.getTotalLength();
for (var i = 0; i < 11; i++) {
var location = path.getPointAtLength(i*totlength/10);
x[i] = location.x;
y[i] = location.y;
if (i > 0) {
asum += x[i - 1]*y[i] - x[i]*y[i - 1];
cxsum += (x[i - 1] + x[i])*(x[i - 1]*y[i] - x[i]*y[i - 1]);
cysum += (y[i - 1] + y[i])*(x[i - 1]*y[i] - x[i]*y[i - 1]);
}
}
return({x: (1/(3*asum))*cxsum, y: (1/(3*asum))*cysum});
}
It's basically an approximation of any path by 10 points (the 11th is equal to the starting point), and the function returns, for that triangle, the coordinates:
Object {x: 65.32077336966377, y: 16.33111549955705}
I've tested it with many other shapes, and it works pretty good.
Hope it helps somebody.
This snippet will calculate the center of any polygon by averaging the vertices.
var paper = Raphael(0,0, 320, 200);
var triangle = "M0,0 L100,0 100,50 z";
var tri = paper.path(triangle);
tri.attr('fill', 'blue');
var center = raphaelPathCenter( tri );
var circle = paper.circle( center.x, center.y, 5);
circle.attr("fill", "#f00");
circle.attr("stroke", "#fff");
function raphaelPathCenter( path ) {
path.getBBox(); // forces path to be traced so realPath is not null.
var vertices = parseSVGVertices( path.realPath );
var center = vertices.reduce( function(prev,cur) {
return { x: prev.x + cur.x, y: prev.y + cur.y }
}, {x:0, y:0} );
center.x /= vertices.length;
center.y /= vertices.length;
return center;
}
function parseSVGVertices( svgPath )
{
var vertices = [];
for ( var i = 0; i < svgPath.length; i ++ )
{
var vertex = svgPath[i];
if ( "ML".indexOf( vertex[0] ) > -1 ) // check SVG command
vertices.push( { x: vertex[1], y: vertex[2] } );
}
return vertices;
}
<script src="https://raw.githubusercontent.com/DmitryBaranovskiy/raphael/master/raphael-min.js"></script>
<canvas id='canvas'></canvas>
<pre id='output'></pre>
However there are a few more triangle centers to choose from.
I've this simple dummy file that I'm using to do some testing. The intended result is to drag the red circle along the path. The thing is that I can't figure out how to associate both shapes.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="raphael-min.js"></script>
</head>
<body>
<script type="text/javascript">
// Creates canvas 320 × 200 at 10, 50
var r = Raphael(10, 50, 320, 200);
var p = r.path("M100,100c0,50 100-50 100,0c0,50 -100-50 -100,0z").attr({stroke: "#ddd"}),
e = r.ellipse(104, 100, 4, 4).attr({stroke: "none", fill: "#f00"}),
/*var c = r.circle(100, 100, 50).attr({
fill: "hsb(.8, 1, 1)",
stroke: "none",
opacity: .5
});*/
var start = function () {
// storing original coordinates
this.ox = this.attr("cx");
this.oy = this.attr("cy");
this.attr({opacity: 1});
},
move = function (dx, dy) {
// move will be called with dx and dy
this.attr({cx: this.ox + dx, cy: this.oy + dy});
},
up = function () {
// restoring state
this.attr({opacity: 1});
};
e.drag(move, start, up);
</script>
</body>
</html>
You didn't specify exactly how you want the interaction to work, so I used what feels most natural to me.
We can assume the dot must remain on the path, so its position must be given by
p.getPointAtLength(l);
for some l. To find l we can search for the local minimum of the distance between the curve and the cursor position. We initialize the search with l0 where l0 is the value of l currently defining the location of the dot.
See the JSfiddle here for a working example:
http://jsfiddle.net/fuzic/kKLtH/
Here is the code:
var searchDl = 1;
var l = 0;
// Creates canvas 320 × 200 at 10, 50
var r = Raphael(10, 50, 320, 200);
var p = r.path("M100,100c0,50 100-50 100,0c0,50 -100-50 -100,0z").attr({stroke: "#ddd"}),
pt = p.getPointAtLength(l);
e = r.ellipse(pt.x, pt.y, 4, 4).attr({stroke: "none", fill: "#f00"}),
totLen = p.getTotalLength(),
start = function () {
// storing original coordinates
this.ox = this.attr("cx");
this.oy = this.attr("cy");
this.attr({opacity: 1});
},
move = function (dx, dy) {
var tmpPt = {
x : this.ox + dx,
y : this.oy + dy
};
l = gradSearch(l, tmpPt);
pt = p.getPointAtLength(l);
this.attr({cx: pt.x, cy: pt.y});
},
up = function () {
this.attr({opacity: 1});
},
gradSearch = function (l0, pt) {
l0 = l0 + totLen;
var l1 = l0,
dist0 = dist(p.getPointAtLength(l0 % totLen), pt),
dist1,
searchDir;
if (dist(p.getPointAtLength((l0 - searchDl) % totLen), pt) >
dist(p.getPointAtLength((l0 + searchDl) % totLen), pt)) {
searchDir = searchDl;
} else {
searchDir = -searchDl;
}
l1 += searchDir;
dist1 = dist(p.getPointAtLength(l1 % totLen), pt);
while (dist1 < dist0) {
dist0 = dist1;
l1 += searchDir;
dist1 = dist(p.getPointAtLength(l1 % totLen), pt);
}
l1 -= searchDir;
return (l1 % totLen);
},
dist = function (pt1, pt2) {
var dx = pt1.x - pt2.x;
var dy = pt1.y - pt2.y;
return Math.sqrt(dx * dx + dy * dy);
};
e.drag(move, start, up);
A circle object has an x,y coordinate for its center, and a radius. To make sure the circle remains on the line, simply find the intersection of the center of the circle and the line itself.
To do this, you will need to store the start and end coordinates of your line. Then using the equation of a line: y = mx + b, you can find the slope and y-intercept. Once you have a function for the line, you can generate new coordinates for the circle by plugging in different values of x.
Also, by plugging in the x,y coordinates of the circle into your function, you can check to see if the circle is on the line.