Prevent relative rotation in Matter.js constraint - javascript

I'm using Matter.js and I want two rectangles with a constraint to make them act if they where a single rigid object.
I am basically setting stiffness to 1, so the contraint acts like a rigid bar instead of a spring.
Also to prevent the object from rotating, I'm setting the intertia to Infinity.
// a 20x20 square with 0 friction and infinite inertia
let objectA = Bodies.rectangle(0, 0, 20, 20, {
frictionAir: 0,
inertia: 'Infinity'
});
let objectB = Bodies.rectangle(30, 0, 20, 20, {
frictionAir: 0,
inertia: 'Infinity'
});
let constraint = Constraint.create({
bodyA: objectB,
bodyB: objectB,
length: 30,
stiffness: 1);
This indeed creates 2 objects with a fixed distance and they do not rotate (both squares always have the same absolute orientation)
However the objects can rotate between them, the constrain acts as a linear constraint but not as an angular constraint.
This picture shows how the distance between objects is kept, how the absolute orientation of the objects has not changed but how the objects rotate around each other.
How can I get rid of this rotation and have the two objects act if they were a single object?

I use a different approach: building a Body from parts instead of using constraints. The result is a single rigid object. Matter handles the parts still separately, so you can e.g. drop a ball in the cart created with the code below.
let cart = bodyWithParts(200, 150, { isStatic: true, friction: 0.0 });
function bodyWithParts(x, y, options) {
options = options || {}
let w = 4;
options.parts = [];
options.parts.push(Matter.Bodies.rectangle(w, 20, 5, 20));
options.parts.push(Matter.Bodies.rectangle(40 - w, 20, 5, 20));
options.parts.push(Matter.Bodies.rectangle(20, 40 - w, 50, 5))
let body = Matter.Body.create(options)
Matter.Body.setPosition(body, { x: x, y: y });
return body;
}

Building a Body to of parts can be useful, however, the strength of the orientation "constraint" cannot be lowered. The orientation stays fixed in any situation.
Therefore, I've written the following TypeScript function which adds two constraints of zero length to two Body objects. Constraints allows us to set the stiffness parameter.
Note that removing one the constraints allows one of the bodies to rotate around its own position ("midpoint") but it cannot change its position relative the other body.
/**
* Adds constraints to `bodyA` and `bodyB` such that their current
* relative position and orientaion is preseved (depending on `stiffness`).
*
* #param bodyA the first body of the constraint
* #param bodyB the second body of the constraint
* #param stiffness the stiffness of the constraint connecting `bodyA` and `bodyB`
* #param offsetA the constraint offset on `bodyA` in its coordinate system
* #param offsetB the constraint offset on `bodyB` in its coordinate system
*/
function addRigidBodyConstraints(
bodyA: Body, bodyB: Body,
stiffness: number = 0.1,
offsetA: Vector = Vector.create(0, 0),
offsetB: Vector = Vector.create(0, 0)
) {
function makeConstraint(posA: Vector, posB: Vector): Constraint {
return Constraint.create({
bodyA: bodyA, bodyB: bodyB,
pointA: posA, pointB: posB,
// stiffness larger than 0.1 is sometimes unstable
stiffness: stiffness
})
}
// add constraints to world or compound body
World.add(world, [
makeConstraint(Vector.sub(bodyB.position, bodyA.position), offsetB),
makeConstraint(offsetA, Vector.sub(bodyA.position, bodyB.position))
])
}

Related

Detect if Matter.js Body is Circle

I want to test if a particular Matter body is a circle or not, as in:
const compounds = Matter.Composite.allBodies(engine.world)
compounds.forEach(compound => compound.parts.forEach(part => {
const isCircle = ???
if (isCircle) console.log(part.id, 'is a circle')
else console.log(part.id, 'is not a circle')
})
I can't find an official way to test if a Matter body was created as a circle. How can I test if a body was created with new Matter.Body.Circle versus another Body constructor?
You can console.log(a_circle) and check for something to identify a circle by.
I think you can check for a_circle.circleRadius or a_circle.label=='Circle Body'
EDIT: I have looked at the source code before posting this. It's a safe bet (for now as there is no documentation) because you can see that otherwise is just a polygon.
Matter.Bodies.circle = function(x, y, radius, options, maxSides) {
options = options || {};
var circle = {
label: 'Circle Body',
circleRadius: radius
};
// approximate circles with polygons until true circles implemented in SAT
maxSides = maxSides || 25;
var sides = Math.ceil(Math.max(10, Math.min(maxSides, radius)));
// optimisation: always use even number of sides (half the number of unique axes)
if (sides % 2 === 1)
sides += 1;
return Bodies.polygon(x, y, sides, radius, Common.extend({}, circle, options));
}
The other answer suggests looking for circle-specific properties. One problem is, these can change in future Matter.js releases. Also, it doesn't make for readable, intuitive code, and can lead to surprising bugs when additional body types wind up containing a property unexpectedly.
Better is to use the internal label property (also suggested in that answer), which should be stable and defaults to the seemingly-useful "Rectangle Body" and "Circle Body". For simple use cases, this works. Since it's possible to set the label to an object to store arbitrary custom data on the body, it's tempting to go further and use labels for just about everything.
However, I generally ignore labels. The reason is that it pushes too much of the client logic into a physics library that's not really designed for entity management. Furthermore, either of these approaches (labels or body-specific properties) involves iterating all of the bodies to filter out the type you're interested in.
Although no context was provided about the app, having to call allBodies often seems like a potential antipattern. It might be time to consider a redesign so you don't have to. What if you have 5 circles and 500 other bodies? Recursively iterating all 500 on every frame just to find the 5 is a huge waste of resources to achieve something that should be easy and efficient.
My preferred solution for entity management is to simply keep track of each type upon creation, putting them into data structures that are tuned to application-specific needs.
For example, the following script shows a method of efficiently determining body type by presenting the body as a key to a pre-built types map.
const engine = Matter.Engine.create();
engine.gravity.y = 0; // enable top-down
const map = {width: 300, height: 300};
const render = Matter.Render.create({
element: document.querySelector("#container"),
engine,
options: {...map, wireframes: false},
});
const rnd = n => ~~(Math.random() * n);
const rects = [...Array(20)].map(() => Matter.Bodies.rectangle(
rnd(map.width), // x
rnd(map.height), // y
rnd(10) + 15, // w
rnd(10) + 15, // h
{
angle: rnd(Math.PI * 2),
render: {fillStyle: "pink"}
}
));
const circles = [...Array(20)].map(() => Matter.Bodies.circle(
rnd(map.width), // x
rnd(map.height), // y
rnd(5) + 10, // r
{render: {fillStyle: "red"}}
));
const walls = [
Matter.Bodies.rectangle(
0, map.height / 2, 20, map.height, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
Matter.Bodies.rectangle(
map.width / 2, 0, map.width, 20, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
Matter.Bodies.rectangle(
map.width, map.height / 2, 20, map.height, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
Matter.Bodies.rectangle(
map.width / 2, map.height, map.width, 20, {
isStatic: true, render: {fillStyle: "yellow"}
}
),
];
const rectangle = Symbol("rectangle");
const circle = Symbol("circle");
const wall = Symbol("wall");
const types = new Map([
...rects.map(e => [e, rectangle]),
...circles.map(e => [e, circle]),
...walls.map(e => [e, wall]),
]);
const bodies = [...types.keys()];
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: document.querySelector("#container")}
);
Matter.Composite.add(engine.world, [
...bodies, mouseConstraint
]);
const runner = Matter.Runner.create();
Matter.Events.on(runner, "tick", event => {
const underMouse = Matter.Query.point(
bodies,
mouseConstraint.mouse.position
);
if (underMouse.length) {
const descriptions = underMouse.map(e =>
types.get(e).description
);
document.querySelector("#type-hover").textContent = `
${descriptions.join(", ")} hovered
`;
}
else {
document.querySelector("#type-hover").textContent = `
[hover a body]
`;
}
if (mouseConstraint.body) {
document.querySelector("#type-click").textContent = `
${types.get(mouseConstraint.body).description} selected
`;
}
else {
document.querySelector("#type-click").textContent = `
[click and drag a body]
`;
}
});
Matter.Render.run(render);
Matter.Runner.run(runner, engine);
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<h3 id="type-click">[click and drag a body]</h3>
<h3 id="type-hover">[hover a body]</h3>
<div id="container"></div>
If creating and destroying bodies can happen dynamically, a function would need to be written to handle data structure bookkeeping.
Another approach that might work well for some apps is to have a few type-specific sets or maps. This allows you to quickly access all entities of a particular type. This becomes particularly useful once you begin composing bodies as properties of custom classes.
There's no reason you can't have both structures--a reverse lookup that gives the type or custom class given a MJS body, and a structure that contains references to all entities or MJS bodies of a particlar type.
The reverse lookup could enable logic like "on click, take an action on a specific MJS body depending on its type" or "on click, locate my custom class/model associated with this MJS body", while collections support logic like "destroy all enemies".
Ideally, code shouldn't be doing much type-checking. With proper OOP design, you can implement classes that respond correctly to methods regardless of their type. For example, if you have Enemy and Ally classes that each respond to a click, you might create a method called handleClick in each class. This allows you to use code like clickedEntity.handleClick(); without having to know whether clickedEntity is an Enemy or Ally, thereby avoiding the need for a "get type" operation entirely.
For more design suggestions for Matter.js projects, see:
How do you access a body by its label in MatterJS?
My own model in matter.js

d3 scaleBy from the left instead of the center

We're using d3 to build a graph and it has two custom buttons to handle the zooming as well as the standard zooming via the mousewheel/trackpad.
let local = this;
let zoomIn = d3.select("#zoom_in");
let zoomOut = d3.select("#zoom_out");
let reset = d3.select("#reset");
zoomIn.on("click", function() {
zoom.scaleBy(local.chart.transition().duration(500), 1.2);
});
zoomOut.on("click", function() {
zoom.scaleBy(local.chart.transition().duration(500), 0.8)
});
This zooms the graph fine (including the brush). However it zooms from the centre.
According to the documentation: https://github.com/d3/d3-zoom#zoom_scaleBy scaling from the centre is the default behaviour unless a position is provided... but we haven't been able to figure out what this parameter should actually be...
We have tried:
zoom.scaleBy(local.chart.transition().duration(500), 0.8, 0);
and also tried to use translateBy: https://github.com/d3/d3-zoom#zoom_translateBy to move it back to the left AFTER the scale:
zoom.scaleBy(local.chart.transition().duration(500), 0.8, 0);
zoom.translateBy(local.chart.transition().duration(500), 0, 0);
But this cancels out the zoom...
Is there any examples of using scaleBy to zoom from a position?
The API docs on zoom.scaleBy() have you covered (emphasis mine):
# zoom.scaleBy(selection, k[, p])
[…]
the p point may be specified either as a two-element array [px,py] or a function.
By providing a two-element array as the third argument to the call you can specify the center for the zooming. For your code this could be specified as follows:
zoom.scaleBy(local.chart.transition().duration(500), 1.2, [0, height / 2]);
// ↑ ↑
// [px,py ]
Similarly, this holds true for zoom.scaleTo(), zoom.translateBy() and zoom.translateTo().

Why does my canvas noise function always only appear red?

I'm trying to apply a noise effect to my canvas, based on a codepen I saw, which in turn appears to be very similar to an SO answer.
I want to produce a "screen" of randomly transparent pixels, but instead of that I get a field that's completely opaque red. I'm hoping someone who is more familiar with either canvas or typed arrays can show me what I'm doing wrong, and maybe help me understand a few of the techniques at play.
I refactored the codepen code significantly, because (for now) I don't care about animating the noise:
/**
* apply a "noise filter" to a rectangular region of the canvas
* #param {Canvas2DContext} ctx - the context to draw on
* #param {Number} x - the x-coordinate of the top-left corner of the region to noisify
* #param {Number} y - the y-coordinate of the top-left corner of the region to noisify
* #param {Number} width - how wide, in canvas units, the noisy region should be
* #param {Number} height - how tall, in canvas units, the noisy region should be
* #effect draws directly to the canvas
*/
function drawNoise( ctx, x, y, width, height ) {
let imageData = ctx.createImageData(width, height)
let buffer32 = new Uint32Array(imageData.data.buffer)
for (let i = 0, len = buffer32.length; i < len; i++) {
buffer32[i] = Math.random() < 0.5
? 0x00000088 // "noise" pixel
: 0x00000000 // non-noise pixel
}
ctx.putImageData(imageData, x, y)
}
From what I can tell, the core of what's happening is that we wrap the ImageData's raw data representation (a series of 8-bit elements that reflect the red, green, blue, and alpha values for each pixel, in series) in a 32-bit array, which allows us to operate on each pixel as a united tuple. We get an array with one element per pixel instead of four elements per pixel.
Then, we iterate through the elements in that array, writing RGBA values to each element (i.e. each pixel) based on our noise logic. The noise logic here is really simple: each pixel has a ~50% chance of being a "noise" pixel.
Noise pixels are assigned the 32-bit value 0x00000088, which (thanks to the 32-bit chunking provided by the array) is equivalent to rgba(0, 0, 0, 0.5), i.e. black, 50% opacity.
Non-noise pixels are assigned the 32-bit value 0x00000000, which is black 0% opacity, i.e. completely transparent.
Interestingly, we don't write the buffer32 to the canvas. Instead, we write the imageData that was used to construct the Uint32Array, leading me to believe that we're mutating the imageData object through some kind of pass-by-reference; I'm not clear exactly why this is. I know how value & reference passing works generally in JS (scalars are passed by value, objects are passed by reference), but in the non-typed array world, the value passed to the array constructor just determines the length of the array. That's evidently not what's happening here.
As noted, instead of a field of black pixels that are either 50% or 100% transparent, I get a field of all solid pixels, all red. Not only do I not expect to see the color red, there's zero evidence of the random color assignment: every pixel is solid red.
By playing with the two hex values, I've discovered that this produces a scattering of red on black that has the right kind of distribution:
buffer32[i] = Math.random() < 0.5
? 0xff0000ff // <-- I'd assume this is solid red
: 0xff000000 // <-- I'd assume this is invisible red
But it's still solid red, on solid black. None of the underlying canvas data shows through the pixels that should be invisible.
Confusingly, I can't get any colors other than red or black. I also can't get any transparency other than 100% opaque. Just to illustrate the disconnect, I've removed the random element and tried writing each of these nine values to every pixel just to see what happens:
buffer32[i] = 0xRrGgBbAa
// EXPECTED // ACTUAL
buffer32[i] = 0xff0000ff // red 100% // red 100%
buffer32[i] = 0x00ff00ff // green 100% // red 100%
buffer32[i] = 0x0000ffff // blue 100% // red 100%
buffer32[i] = 0xff000088 // red 50% // blood red; could be red on black at 50%
buffer32[i] = 0x00ff0088 // green 50% // red 100%
buffer32[i] = 0x0000ff88 // blue 50% // red 100%
buffer32[i] = 0xff000000 // red 0% // black 100%
buffer32[i] = 0x00ff0000 // green 0% // red 100%
buffer32[i] = 0x0000ff00 // blue 0% // red 100%
What's going on?
EDIT: similar (bad) results after dispensing with the Uint32Array and the spooky mutation, based on the MDN article on ImageData.data:
/**
* fails in exactly the same way
*/
function drawNoise( ctx, x, y, width, height ) {
let imageData = ctx.createImageData(width, height)
for (let i = 0, len = imageData.data.length; i < len; i += 4) {
imageData.data[i + 0] = 0
imageData.data[i + 1] = 0
imageData.data[i + 2] = 0
imageData.data[i + 3] = Math.random() < 0.5 ? 255 : 0
}
ctx.putImageData(imageData, x, y)
}
[TLDR]:
Your Hardware's endianness is designed as LittleEndian and thus the correct Hex format is 0xAABBGGRR, not 0xRRGGBBAA.
First let's explain the "magic" behind TypedArrays: ArrayBuffers.
An ArrayBuffer is a very special object which is directly linked to the device's memory. In itself the ArrayBuffer interface doesn't have too much features for us, but when you create one, you actually allocated its length in memory, for your own script. That is, the js engine won't deal with reallocating it, moving it somewhere else, chunking it and all these slow operations like it does with usual JS objects.
This thus makes it one of the fastest objects to manipulate binary data.
However, as said before, its interface is in itself quite limited. We have no way to access the data directly from the ArrayBuffer, to do this we have to use a view object, which won't copy the data, but really just offer a mean to access it directly.
You can have different views over the same ArrayBuffer, but the data used will always just be the one of the ArrayBuffer, and if you do edit an ArrayBuffer from one view, then it will be visible from the other:
const buffer = new ArrayBuffer(4);
const view1 = new Uint8Array(buffer);
const view2 = new Uint8Array(buffer);
console.log('view1', ...view1); // [0,0,0,0]
console.log('view2', ...view2); // [0,0,0,0]
// we modify only view1
view1[2] = 125;
console.log('view1', ...view1); // [0,0,125,0]
console.log('view2', ...view2); // [0,0,125,0]
There are different kind of view objects, and each will offer different ways to represent the binary data that is assigned to the memory slot allocated by the ArrayBuffer.
TypedArrays like Uint8Array, Float32Array etc. are ArrayLike interfaces which offer an easy way to manipulate the data as an Array, representing the data in their own format (8bits, Float32 etc.).
The DataView interface allows for more open manipulations like reading in different formats even from normally invalid boundaries, however, it comes at the cost of performance.
The ImageData interface itself uses an ArrayBuffer to store its pixel data. By default, it exposes an Uint8ClampedArray view over this data. That is, an ArrayLike object, with each 32bits pixel represented as values from 0 to 255 for each channel Red, Green, Blue and Alpha, in this order.
So your code is taking advantage of the fact TypedArrays are only view objects and that having an other view over the underlying ArrayBuffer will modify it directly.
Its author chose to use an Uint32Array because its a way to set a full pixel (remember canvas image is 32bits) in a single shot. You can reduce the work needed by four time.
However, doing so, you start dealing with 32bits values. And this may come a bit problematic, because now endianness matters.
The Uint8Array [0x00, 0x11, 0x22, 0x33] will be represented as the 32bits value 0x00112233 in BigEndian systems, but as 0x33221100 in LittleEndian ones.
const buff = new ArrayBuffer(4);
const uint8 = new Uint8Array(buff);
const uint32 = new Uint32Array(buff);
uint8[0] = 0x00;
uint8[1] = 0x11;
uint8[2] = 0x22;
uint8[3] = 0x33;
const hex32 = uint32[0].toString(16);
console.log(hex32, hex32 === "33221100" ? 'LE' : 'BE');
Note that most personal hardware are LittleEndian, so it's no surprise if your computer also is.
So with all this, I hope you do know how to fix your code: to generate the color rgba(0,0,0,.5), you need to set the Uint32 value 0x80000000
drawNoise(canvas.getContext('2d'), 0, 0, 300, 150);
function drawNoise(ctx, x, y, width, height) {
const imageData = ctx.createImageData(width, height)
const buffer32 = new Uint32Array(imageData.data.buffer)
const LE = isLittleEndian();
// 0xAABBRRGG : 0xRRGGBBAA;
const black = LE ? 0x80000000 : 0x00000080;
const blue = LE ? 0xFFFF0000 : 0x0000FFFF;
for (let i = 0, len = buffer32.length; i < len; i++) {
buffer32[i] = Math.random() < 0.5
? black
: blue
}
ctx.putImageData(imageData, x, y)
}
function isLittleEndian() {
const uint8 = new Uint8Array(8);
const uint32 = new Uint32Array(uint8.buffer);
uint8[0] = 255;
return uint32[0] === 0XFF;
}
<canvas id="canvas"></canvas>

physics.js attractors, zero gravity and slowing down velocity

I've been experimenting with attractors in physics.js, rigging up a simple object in zero gravity, with an attractor at a point. This creates a great little 'gravity well' as can be seen here.
Where the simple square vector attracts towards a point, at 'x':200,'y':200, and then orbits around it. I'm looking for a way to turn this attractor into more of a gravity well, so that the objects attracted to it slow down over time and eventually settle static and stationary at the point of the attractor, until it was collided with or dragged with the mouse again. Is this a possibility?
Currently the object is created with:
var bodies = [Physics.body('convex-polygon', {
// place the center of the square at (0, 0)
x: 150,
treatment : 'dynamic',
cof: 0.01,
mass : 1,
y: 100,
vertices: [
{ x: 0, y: 0 },
{ x: 0, y: 200 },
{ x: 200, y: 200 },
{ x: 200, y: 0 }
]
})];
The attractor is created thusly:
var attractor = Physics.behavior('attractor', {
order: 0,
strength: 0.0005
}).applyTo(bodies);
attractor.position({'x':200,'y':200});
Affecting the strength of the attractor doesn't appear to help, it just changes the speed of the attraction and subsequent orbit.
I'm looking for a way, in effect, to add friction to the entire space, which I think will do the job in naturally slowing down the object so it ends up stationary at the attractor point. Not sure how to go about this with PhysicsJS.
There is the possibility to create your own Attractor-Behaviour: See this documentation.
If you don't want to that, you can set the option min of the attractor to the size of the polygon, so the forces stops, when the body is at the center of the attractor. Strength and order are optional options, so you don't need to specify them (According to the API). For example this should work for you:
world.add(Physics.behavior("attractor", {
min: 200,
pos: {"x": 200, "y": 200}
}));

mouse position to isometric tile including height

Struggeling translating the position of the mouse to the location of the tiles in my grid. When it's all flat, the math looks like this:
this.position.x = Math.floor(((pos.y - 240) / 24) + ((pos.x - 320) / 48));
this.position.y = Math.floor(((pos.y - 240) / 24) - ((pos.x - 320) / 48));
where pos.x and pos.y are the position of the mouse, 240 and 320 are the offset, 24 and 48 the size of the tile. Position then contains the grid coordinate of the tile I'm hovering over. This works reasonably well on a flat surface.
Now I'm adding height, which the math does not take into account.
This grid is a 2D grid containing noise, that's being translated to height and tile type. Height is really just an adjustment to the 'Y' position of the tile, so it's possible for two tiles to be drawn in the same spot.
I don't know how to determine which tile I'm hovering over.
edit:
Made some headway... Before, I was depending on the mouseover event to calculate grid position. I just changed this to do the calculation in the draw loop itself, and check if the coordinates are within the limits of the tile currently being drawn. creates some overhead tho, not sure if I'm super happy with it but I'll confirm if it works.
edit 2018:
I have no answer, but since this ha[sd] an open bounty, help yourself to some code and a demo
The grid itself is, simplified;
let grid = [[10,15],[12,23]];
which leads to a drawing like:
for (var i = 0; i < grid.length; i++) {
for (var j = 0; j < grid[0].length; j++) {
let x = (j - i) * resourceWidth;
let y = ((i + j) * resourceHeight) + (grid[i][j] * -resourceHeight);
// the "+" bit is the adjustment for height according to perlin noise values
}
}
edit post-bounty:
See GIF. The accepted answer works. The delay is my fault, the screen doesn't update on mousemove (yet) and the frame rate is low-ish. It's clearly bringing back the right tile.
Source
Intresting task.
Lets try to simplify it - lets resolve this concrete case
Solution
Working version is here: https://github.com/amuzalevskiy/perlin-landscape (changes https://github.com/jorgt/perlin-landscape/pull/1 )
Explanation
First what came into mind is:
Just two steps:
find an vertical column, which matches some set of tiles
iterate tiles in set from bottom to top, checking if cursor is placed lower than top line
Step 1
We need two functions here:
Detects column:
function getColumn(mouseX, firstTileXShiftAtScreen, columnWidth) {
return (mouseX - firstTileXShiftAtScreen) / columnWidth;
}
Function which extracts an array of tiles which correspond to this column.
Rotate image 45 deg in mind. The red numbers are columnNo. 3 column is highlighted. X axis is horizontal
function tileExists(x, y, width, height) {
return x >= 0 & y >= 0 & x < width & y < height;
}
function getTilesInColumn(columnNo, width, height) {
let startTileX = 0, startTileY = 0;
let xShift = true;
for (let i = 0; i < columnNo; i++) {
if (tileExists(startTileX + 1, startTileY, width, height)) {
startTileX++;
} else {
if (xShift) {
xShift = false;
} else {
startTileY++;
}
}
}
let tilesInColumn = [];
while(tileExists(startTileX, startTileY, width, height)) {
tilesInColumn.push({x: startTileX, y: startTileY, isLeft: xShift});
if (xShift) {
startTileX--;
} else {
startTileY++;
}
xShift = !xShift;
}
return tilesInColumn;
}
Step 2
A list of tiles to check is ready. Now for each tile we need to find a top line. Also we have two types of tiles: left and right. We already stored this info during building matching tiles set.
function getTileYIncrementByTileZ(tileZ) {
// implement here
return 0;
}
function findExactTile(mouseX, mouseY, tilesInColumn, tiles2d,
firstTileXShiftAtScreen, firstTileYShiftAtScreenAt0Height,
tileWidth, tileHeight) {
// we built a set of tiles where bottom ones come first
// iterate tiles from bottom to top
for(var i = 0; i < tilesInColumn; i++) {
let tileInfo = tilesInColumn[i];
let lineAB = findABForTopLineOfTile(tileInfo.x, tileInfo.y, tiles2d[tileInfo.x][tileInfo.y],
tileInfo.isLeft, tileWidth, tileHeight);
if ((mouseY - firstTileYShiftAtScreenAt0Height) >
(mouseX - firstTileXShiftAtScreen)*lineAB.a + lineAB.b) {
// WOHOO !!!
return tileInfo;
}
}
}
function findABForTopLineOfTile(tileX, tileY, tileZ, isLeftTopLine, tileWidth, tileHeight) {
// find a top line ~~~ a,b
// y = a * x + b;
let a = tileWidth / tileHeight;
if (isLeftTopLine) {
a = -a;
}
let b = isLeftTopLine ?
tileY * 2 * tileHeight :
- (tileX + 1) * 2 * tileHeight;
b -= getTileYIncrementByTileZ(tileZ);
return {a: a, b: b};
}
Please don't judge me as I am not posting any code. I am just suggesting an algorithm that can solve it without high memory usage.
The Algorithm:
Actually to determine which tile is on mouse hover we don't need to check all the tiles. At first we think the surface is 2D and find which tile the mouse pointer goes over with the formula OP posted. This is the farthest probable tile mouse cursor can point at this cursor position.
This tile can receive mouse pointer if it's at 0 height, by checking it's current height we can verify if this is really at the height to receive pointer, we mark it and move forward.
Then we find the next probable tile which is closer to the screen by incrementing or decrementing x,y grid values depending on the cursor position.
Then we keep on moving forward in a zigzag fashion until we reach a tile which cannot receive pointer even if it is at it's maximum height.
When we reach this point the last tile found that were at a height to receive pointer is the tile that we are looking for.
In this case we only checked 8 tiles to determine which tile is currently receiving pointer. This is very memory efficient in comparison to checking all the tiles present in the grid and yields faster result.
One way to solve this would be to follow the ray that goes from the clicked pixel on the screen into the map. For that, just determine the camera position in relation to the map and the direction it is looking at:
const camPos = {x: -5, y: -5, z: -5}
const camDirection = { x: 1, y:1, z:1}
The next step is to get the touch Position in the 3D world. In this certain perspective that is quite simple:
const touchPos = {
x: camPos.x + touch.x / Math.sqrt(2),
y: camPos.y - touch.x / Math.sqrt(2),
z: camPos.z - touch.y / Math.sqrt(2)
};
Now you just need to follow the ray into the layer (scale the directions so that they are smaller than one of your tiles dimensions):
for(let delta = 0; delta < 100; delta++){
const x = touchPos.x + camDirection.x * delta;
const y = touchPos.y + camDirection.y * delta;
const z = touchPos.z + camDirection.z * delta;
Now just take the tile at xz and check if y is smaller than its height;
const absX = ~~( x / 24 );
const absZ = ~~( z / 24 );
if(tiles[absX][absZ].height >= y){
// hanfle the over event
}
I had same situation on a game. first I tried with mathematics, but when I found that the clients wants to change the map type every day, I changed the solution with some graphical solution and pass it to the designer of the team. I captured the mouse position by listening the SVG elements click.
the main graphic directly used to capture and translate the mouse position to my required pixel.
https://blog.lavrton.com/hit-region-detection-for-html5-canvas-and-how-to-listen-to-click-events-on-canvas-shapes-815034d7e9f8
https://code.sololearn.com/Wq2bwzSxSnjl/#html
Here is the grid input I would define for the sake of this discussion. The output should be some tile (coordinate_1, coordinate_2) based on visibility on the users screen of the mouse:
I can offer two solutions from different perspectives, but you will need to convert this back into your problem domain. The first methodology is based on coloring tiles and can be more useful if the map is changing dynamically. The second solution is based on drawing coordinate bounding boxes based on the fact that tiles closer to the viewer like (0, 0) can never be occluded by tiles behind it (1,1).
Approach 1: Transparently Colored Tiles
The first approach is based on drawing and elaborated on here. I must give the credit to #haldagan for a particularly beautiful solution. In summary it relies on drawing a perfectly opaque layer on top of the original canvas and coloring every tile with a different color. This top layer should be subject to the same height transformations as the underlying layer. When the mouse hovers over a particular layer you can detect the color through canvas and thus the tile itself. This is the solution I would probably go with and this seems to be a not so rare issue in computer visualization and graphics (finding positions in a 3d isometric world).
Approach 2: Finding the Bounding Tile
This is based on the conjecture that the "front" row can never be occluded by "back" rows behind it. Furthermore, "closer to the screen" tiles cannot be occluded by tiles "farther from the screen". To make precise the meaning of "front", "back", "closer to the screen" and "farther from the screen", take a look at the following:
.
Based on this principle the approach is to build a set of polygons for each tile. So firstly we determine the coordinates on the canvas of just box (0, 0) after height scaling. Note that the height scale operation is simply a trapezoid stretched vertically based on height.
Then we determine the coordinates on the canvas of boxes (1, 0), (0, 1), (1, 1) after height scaling (we would need to subtract anything from those polygons which overlap with the polygon (0, 0)).
Proceed to build each boxes bounding coordinates by subtracting any occlusions from polygons closer to the screen, to eventually get coordinates of polygons for all boxes.
With these coordinates and some care you can ultimately determine which tile is pointed to by a binary search style through overlapping polygons by searching through bottom rows up.
It also matters what else is on the screen. Maths attempts work if your tiles are pretty much uniform. However if you are displaying various objects and want the user to pick them, it is far easier to have a canvas-sized map of identifiers.
function poly(ctx){var a=arguments;ctx.beginPath();ctx.moveTo(a[1],a[2]);
for(var i=3;i<a.length;i+=2)ctx.lineTo(a[i],a[i+1]);ctx.closePath();ctx.fill();ctx.stroke();}
function circle(ctx,x,y,r){ctx.beginPath();ctx.arc(x,y,r,0,2*Math.PI);ctx.fill();ctx.stroke();}
function Tile(h,c,f){
var cnv=document.createElement("canvas");cnv.width=100;cnv.height=h;
var ctx=cnv.getContext("2d");ctx.lineWidth=3;ctx.lineStyle="black";
ctx.fillStyle=c;poly(ctx,2,h-50,50,h-75,98,h-50,50,h-25);
poly(ctx,50,h-25,2,h-50,2,h-25,50,h-2);
poly(ctx,50,h-25,98,h-50,98,h-25,50,h-2);
f(ctx);return ctx.getImageData(0,0,100,h);
}
function put(x,y,tile,image,id,map){
var iw=image.width,tw=tile.width,th=tile.height,bdat=image.data,fdat=tile.data;
for(var i=0;i<tw;i++)
for(var j=0;j<th;j++){
var ijtw4=(i+j*tw)*4,a=fdat[ijtw4+3];
if(a!==0){
var xiyjiw=x+i+(y+j)*iw;
for(var k=0;k<3;k++)bdat[xiyjiw*4+k]=(bdat[xiyjiw*4+k]*(255-a)+fdat[ijtw4+k]*a)/255;
bdat[xiyjiw*4+3]=255;
map[xiyjiw]=id;
}
}
}
var cleanimage;
var pickmap;
function startup(){
var water=Tile(77,"blue",function(){});
var field=Tile(77,"lime",function(){});
var tree=Tile(200,"lime",function(ctx){
ctx.fillStyle="brown";poly(ctx,50,50,70,150,30,150);
ctx.fillStyle="forestgreen";circle(ctx,60,40,30);circle(ctx,68,70,30);circle(ctx,32,60,30);
});
var sheep=Tile(200,"lime",function(ctx){
ctx.fillStyle="white";poly(ctx,25,155,25,100);poly(ctx,75,155,75,100);
circle(ctx,50,100,45);circle(ctx,50,80,30);
poly(ctx,40,70,35,80);poly(ctx,60,70,65,80);
});
var cnv=document.getElementById("scape");
cnv.width=500;cnv.height=400;
var ctx=cnv.getContext("2d");
cleanimage=ctx.getImageData(0,0,500,400);
pickmap=new Uint8Array(500*400);
var tiles=[water,field,tree,sheep];
var map=[[[0,0],[1,1],[1,1],[1,1],[1,1]],
[[0,0],[1,1],[1,2],[3,2],[1,1]],
[[0,0],[1,1],[2,2],[3,2],[1,1]],
[[0,0],[1,1],[1,1],[1,1],[1,1]],
[[0,0],[0,0],[0,0],[0,0],[0,0]]];
for(var x=0;x<5;x++)
for(var y=0;y<5;y++){
var desc=map[y][x],tile=tiles[desc[0]];
put(200+x*50-y*50,200+x*25+y*25-tile.height-desc[1]*20,
tile,cleanimage,x+1+(y+1)*10,pickmap);
}
ctx.putImageData(cleanimage,0,0);
}
var mx,my,pick;
function mmove(event){
mx=Math.round(event.offsetX);
my=Math.round(event.offsetY);
if(mx>=0 && my>=0 && mx<cleanimage.width && my<cleanimage.height && pick!==pickmap[mx+my*cleanimage.width])
requestAnimationFrame(redraw);
}
function redraw(){
pick=pickmap[mx+my*cleanimage.width];
document.getElementById("pick").innerHTML=pick;
var ctx=document.getElementById("scape").getContext("2d");
ctx.putImageData(cleanimage,0,0);
if(pick!==0){
var temp=ctx.getImageData(0,0,cleanimage.width,cleanimage.height);
for(var i=0;i<pickmap.length;i++)
if(pickmap[i]===pick)
temp.data[i*4]=255;
ctx.putImageData(temp,0,0);
}
}
startup(); // in place of body.onload
<div id="pick">Move around</div>
<canvas id="scape" onmousemove="mmove(event)"></canvas>
Here the "id" is a simple x+1+(y+1)*10 (so it is nice when displayed) and fits into a byte (Uint8Array), which could go up to 15x15 display grid already, and there are wider types available too.
(Tried to draw it small, and it looked ok on the snippet editor screen but apparently it is still too large here)
Computer graphics is fun, right?
This is a special case of the more standard computational geometry "point location problem". You could also express it as a nearest neighbour search.
To make this look like a point location problem you just need to express your tiles as non-overlapping polygons in a 2D plane. If you want to keep your shapes in a 3D space (e.g. with a z buffer) this becomes the related "ray casting problem".
One source of good geometry algorithms is W. Randolf Franklin's website and turf.js contains an implementation of his PNPOLY algorithm.
For this special case we can be even faster than the general algorithms by treating our prior knowledge about the shape of the tiles as a coarse R-tree (a type of spatial index).

Categories

Resources