Related
I have an object rendered to a canvas. I'm trying to get the object to move along a set path on a loop. Here is what I have:
// Canvas Element
var canvas = null;
// Canvas Draw
var ctx = null;
// Static Globals
var tileSize = 16,
mapW = 10,
mapH = 10;
// Instances of entities
var entities = [
// A single entity that starts at tile 28, and uses the setPath() function
{
id: 0,
tile: 28,
xy: tileToCoords(28),
width: 16,
height: 24,
speedX: 0,
speedY: 0,
logic: {
func: 'setPath',
// These are the parameters that go into the setPath() function
data: [0, ['down', 'up', 'left', 'right'], tileToCoords(28), 0]
},
dir: {up:false, down:false, left:false, right:false}
}
];
// Array for tile data
var map = [];
window.onload = function(){
// Populate the map array with a blank map and 4 walls
testMap();
canvas = document.getElementById('save');
ctx = canvas.getContext("2d");
// Add all the entities to the map array and start their behavior
for(var i = 0; i < entities.length; ++i){
map[entities[i].tile].render.object = entities[i].id;
if(entities[i].logic){
window[entities[i].logic.func].apply(null, entities[i].logic.data);
}
}
drawGame(map);
window.requestAnimationFrame(function(){
mainLoop();
});
};
function drawGame(map){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// We save all the entity data for later so the background colors don't get rendered on top
var tileObjData = [];
for(var y = 0; y < mapH; ++y){
for(var x = 0; x < mapW; ++x){
var currentPos = ((y*mapW)+x);
ctx.fillStyle = map[currentPos].render.base;
ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);
var thisObj = map[currentPos].render.object;
if(thisObj !== false){
thisObj = entities[thisObj];
var originX = thisObj.xy.x;
var originY = thisObj.xy.y;
tileObjData.push(
{
id: thisObj.id,
originX: originX,
originY: originY,
width: thisObj.width,
height: thisObj.height,
}
);
}
}
}
// Draw all the entities after the background tiles are drawn
for(var i = 0; i < tileObjData.length; ++i){
drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
}
}
// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){
var offX = posX + entities[id].speedX;
var offY = posY + entities[id].speedY;
ctx.fillStyle = '#00F';
ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);
entities[id].xy.x = offX;
entities[id].xy.y = offY;
}
// Redraws the canvas with the browser framerate
function mainLoop(){
drawGame(map);
for(var i = 0; i < entities.length; ++i){
animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
}
window.requestAnimationFrame(function(){
mainLoop();
});
}
// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){
var prevTile = entities[id].tile;
if(up){
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = -1;
}
}
else if(down){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = 1;
}
}
else{
entities[id].speedY = 0;
}
if(left){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = -1;
}
}
else if(right){
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = 1;
}
}
else{
entities[id].speedX = 0;
}
entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
map[entities[id].tile].render.object = id;
if(prevTile !== entities[id].tile){
map[prevTile].render.object = false;
}
}
//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array
function setPath(id, path, originPoint, step){
// Determine if the entity has travelled one tile from the origin
var destX = Math.abs(entities[id].xy.x - originPoint.x);
var destY = Math.abs(entities[id].xy.y - originPoint.y);
if(destX >= tileSize || destY >= tileSize){
// Go to the next step in the path array
step = step + 1;
if(step >= path.length){
step = 0;
}
// Reset the origin to the current tile coordinates
originPoint = entities[id].xy;
}
// Set the direction based on the current index of the path array
switch(path[step]) {
case 'up':
entities[id].dir.up = true;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'down':
entities[id].dir.up = false;
entities[id].dir.down = true;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'left':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = true;
entities[id].dir.right = false;
break;
case 'right':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = true;
break;
};
window.requestAnimationFrame(function(){
setPath(id, path, originPoint, step);
});
}
// Take a tile index and return x,y coordinates
function tileToCoords(tile){
var yIndex = Math.floor(tile / mapW);
var xIndex = tile - (yIndex * mapW);
var y = yIndex * tileSize;
var x = xIndex * tileSize;
return {x:x, y:y};
}
// Take x,y coordinates and return a tile index
function coordsToTile(x, y){
var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
return tile;
}
// Generate a map array with a blank map and 4 walls
function testMap(){
for(var i = 0; i < (mapH * mapW); ++i){
// Edges
if (
// top
i < mapW ||
// left
(i % mapW) == 0 ||
// right
((i + 1) % mapW) == 0 ||
// bottom
i > ((mapW * mapH) - mapW)
) {
map.push(
{
id: i,
render: {
base: '#D35',
object: false,
sprite: false
},
state: {
passable: false
}
},
);
}
else{
// Grass
map.push(
{
id: i,
render: {
base: '#0C3',
object: false,
sprite: false
},
state: {
passable: true
}
},
);
}
}
}
<!DOCTYPE html>
<html>
<head>
<style>
body{
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
font-size: 18px;
padding: 0;
margin: 0;
}
main{
width: 100%;
max-width: 800px;
margin: 10px auto;
display: flex;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
}
.game{
width: 1000px;
height: 1000px;
position: relative;
}
canvas{
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.game canvas{
position: absolute;
top: 0;
left: 0;
width: 800px;
height: 800px;
}
</style>
</head>
<body>
<main>
<div class="game">
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
</div>
</main>
</body>
</html>
The problem is with the setPath() function, and more specifically I think it's something with the originPoint variable. The idea is that setPath() moves the object one tile per path string, and originPoint should be the coordinates of the last tile visited (so it should only get updated once the object coordinates are one tile length away from the originPoint). Right now it only gets updated the first time and then stops. Hopefully someone can point out what I got wrong here.
Your condition to change the path direction I change it to have conditions for each direction, something like:
if ((entities[id].dir.left && entities[id].xy.x <= tileSize) ||
(entities[id].dir.right && entities[id].xy.x >= tileSize*8) ||
(entities[id].dir.up && entities[id].xy.y <= tileSize) ||
(entities[id].dir.down && entities[id].xy.y >= tileSize*8)) {
and the originPoint was just a reference you should do:
originPoint = JSON.parse(JSON.stringify(entities[id].xy));
See the working code below
// Canvas Element
var canvas = null;
// Canvas Draw
var ctx = null;
// Static Globals
var tileSize = 16,
mapW = 10,
mapH = 10;
// Instances of entities
var entities = [
// A single entity that starts at tile 28, and uses the setPath() function
{
id: 0,
tile: 28,
xy: tileToCoords(28),
width: 16,
height: 24,
speedX: 0,
speedY: 0,
logic: {
func: 'setPath',
// These are the parameters that go into the setPath() function
data: [0, ['down', 'left', 'down', 'left', 'up', 'left', 'left', 'right', 'up', 'right', 'down','right', "up"], tileToCoords(28), 0]
},
dir: {up:false, down:false, left:false, right:false}
}
];
// Array for tile data
var map = [];
window.onload = function(){
// Populate the map array with a blank map and 4 walls
testMap();
canvas = document.getElementById('save');
ctx = canvas.getContext("2d");
// Add all the entities to the map array and start their behavior
for(var i = 0; i < entities.length; ++i){
map[entities[i].tile].render.object = entities[i].id;
if(entities[i].logic){
window[entities[i].logic.func].apply(null, entities[i].logic.data);
}
}
drawGame(map);
window.requestAnimationFrame(function(){
mainLoop();
});
};
function drawGame(map){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// We save all the entity data for later so the background colors don't get rendered on top
var tileObjData = [];
for(var y = 0; y < mapH; ++y){
for(var x = 0; x < mapW; ++x){
var currentPos = ((y*mapW)+x);
ctx.fillStyle = map[currentPos].render.base;
ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);
var thisObj = map[currentPos].render.object;
if(thisObj !== false){
thisObj = entities[thisObj];
var originX = thisObj.xy.x;
var originY = thisObj.xy.y;
tileObjData.push(
{
id: thisObj.id,
originX: originX,
originY: originY,
width: thisObj.width,
height: thisObj.height,
}
);
}
}
}
// Draw all the entities after the background tiles are drawn
for(var i = 0; i < tileObjData.length; ++i){
drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
}
}
// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){
var offX = posX + entities[id].speedX;
var offY = posY + entities[id].speedY;
ctx.fillStyle = '#00F';
ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);
entities[id].xy.x = offX;
entities[id].xy.y = offY;
}
// Redraws the canvas with the browser framerate
function mainLoop(){
drawGame(map);
for(var i = 0; i < entities.length; ++i){
animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
}
window.requestAnimationFrame(function(){
mainLoop();
});
}
// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){
var prevTile = entities[id].tile;
if(up){
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = -1;
}
}
else if(down){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = 1;
}
}
else{
entities[id].speedY = 0;
}
if(left){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = -1;
}
}
else if(right){
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = 1;
}
}
else{
entities[id].speedX = 0;
}
entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
map[entities[id].tile].render.object = id;
if(prevTile !== entities[id].tile){
map[prevTile].render.object = false;
}
}
//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array
function setPath(id, path, originPoint, step){
if ((entities[id].dir.left && entities[id].xy.x <= originPoint.x - tileSize) ||
(entities[id].dir.right && entities[id].xy.x >= originPoint.x + tileSize) ||
(entities[id].dir.up && entities[id].xy.y <= originPoint.y - tileSize) ||
(entities[id].dir.down && entities[id].xy.y >= originPoint.y + tileSize)) {
// Go to the next step in the path array
step = step + 1;
if(step >= path.length){
step = 0;
}
// Reset the origin to the current tile coordinates
originPoint = JSON.parse(JSON.stringify(entities[id].xy));
}
// Set the direction based on the current index of the path array
switch(path[step]) {
case 'up':
entities[id].dir.up = true;
entities[id].dir.down = false;
entities[id].dir.left = false
entities[id].dir.right = false;
break;
case 'down':
entities[id].dir.up = false;
entities[id].dir.down = true;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'left':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = true;
entities[id].dir.right = false;
break;
case 'right':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = true;
break;
};
window.requestAnimationFrame(function(){
setPath(id, path, originPoint, step);
});
}
// Take a tile index and return x,y coordinates
function tileToCoords(tile){
var yIndex = Math.floor(tile / mapW);
var xIndex = tile - (yIndex * mapW);
var y = yIndex * tileSize;
var x = xIndex * tileSize;
return {x:x, y:y};
}
// Take x,y coordinates and return a tile index
function coordsToTile(x, y){
var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
return tile;
}
// Generate a map array with a blank map and 4 walls
function testMap(){
for(var i = 0; i < (mapH * mapW); ++i){
// Edges
if (
// top
i < mapW ||
// left
(i % mapW) == 0 ||
// right
((i + 1) % mapW) == 0 ||
// bottom
i > ((mapW * mapH) - mapW)
) {
map.push(
{
id: i,
render: {
base: '#D35',
object: false,
sprite: false
},
state: {
passable: false
}
},
);
}
else{
// Grass
map.push(
{
id: i,
render: {
base: '#0C3',
object: false,
sprite: false
},
state: {
passable: true
}
},
);
}
}
}
<!DOCTYPE html>
<html>
<head>
<style>
body{
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
font-size: 18px;
padding: 0;
margin: 0;
}
main{
width: 100%;
max-width: 800px;
margin: 10px auto;
display: flex;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
}
.game{
width: 1000px;
height: 1000px;
position: relative;
}
canvas{
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.game canvas{
position: absolute;
top: 0;
left: 0;
width: 800px;
height: 800px;
}
</style>
</head>
<body>
<main>
<div class="game">
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
</div>
</main>
</body>
</html>
As someone has already solved your bug...
This is more than a solution to your problem as the real problem you are facing is complexity, long complex if statements using data structures representing the same information in different ways making it difficult to see simple errors in logic.
On top of that you have some poor style habits that compound the problem.
A quick fix will just mean you will be facing the next problem sooner. You need to write in a ways that reduces the chances of logic errors due to increasing complexity
Style
First style. Good style is very important
Don't assign null to declared variables. JavaScript should not need to use null, the exception to the rule is that some C++ coders infected the DOM API with null returns because they did not understand JavaScipt (or was a cruel joke), and now we are stuck with null
window is the default this (global this) and is seldom needed. Eg window.requestAnimationFrame is identical to just requestAnimationFrame and window.onload is identical to onload
Don't pollute your code with inaccurate, redundant and/or obvious comments, use good naming to provide the needed information. eg:
var map[]; has the comment // array of tile data Well really its an array that has data, who would have guessed, so the comment can be // tiles but then map is a rather ambiguous name. Remove the comment and give the variable a better name.
The comment // Static Globals above some vars. Javascript does not have static as a type so the comment is wrong and the "global's" part is "duh..."
Use const to declare constants, move all the magic numbers to the top and define them as named const. A name has meaning, an number in some code has next to no meaning.
Don't assign listener to the event name, it is unreliable and can be hijacked or overwritten. Always use addEventListener to assign an event listener
Be very careful with your naming. eg the function named coordsToTile is confusing as it does not return a tile, it returns a tile index, either change the function name to match the functions behavior, or change the behavior to match the name.
Don't use redundant intermediate functions, examples:
Your frame request requestAnimationFrame(function(){mainLoop()}); should skip the middle man and be requestAnimationFrame(mainLoop);
You use Function.apply to call the function window[entities[i].logic.func].apply(null, entities[i].logic.data);. apply is used to bind context this to the call, you don't use this in the function so you don't need use the apply. eg window[entities[i].logic.func](...entities[i].logic.data);
BTW being forced to use bracket notation to access a global is a sign of poor data structure. You should never do that.
JavaScript has an unofficial idiomatic styles, you should try to write JS in this style. Some examples from your code
else on the same line as closing }
Space after if, else, for, function() and befor else, opening block {
An id and an index are not the same, use idx or index for an index and id for an identifier
Keep it simple
The more complex you make your data structures the harder it is for you to maintain them.
Structured
Define objects to encapsulate and organize your data.
A global config object, that is transprotable ie can converted be to and from JSON. it contains all the magic numbers, defaults, type descriptions, and what not needed in the game.
Create a set of global utilities that do common repeated tasks, ie create coordinates, list of directions.
Define object that encapsulate the settings and behaviors specific only to that object.
Use polymorphic object design, meaning that different objects use named common behaviors and properties. In the example all drawable object have a function called draw that takes an argument ctx, all objects that can be updated have a function called update
Example
This example is a complete rewrite of your code and fixing your problem. It may be a little advanced, but it is only an example to look though an pick up some tips.
A quick description of the objects used.
Objects
config is transportable config data
testMap is an example map description
tileMap does map related stuff
Path Object encapsulating path logic
Entity Object a single moving entity
Tile Object representing a single tile
game The game state manager
Games have states, eg loading, intro, inPlay, gameOver etc. If you do not plan ahead and create a robust state manager you will find it very difficult to move from one state to the next
I have included the core of a finite state manager. The state manager is responsible for updating and rendering. it is also responsible for all state changes.
setTimeout(() => game.state = "setup", 0); // Will start the game
const canvas = document.getElementById('save');
const ctx = canvas.getContext("2d");
const point = (x = 0, y = 0) => ({x,y});
const dirs = Object.assign(
[point(0, -1), point(1), point(0,1), point(-1)], { // up, right, down, left
"u": 0, // defines index for direction string characters
"r": 1,
"d": 2,
"l": 3,
strToDirIdxs(str) { return str.toLowerCase().split("").map(char => dirs[char]) },
}
);
const config = {
pathIdx: 28,
pathTypes: {
standard: "dulr",
complex: "dulrldudlruldrdlrurdlurd",
},
tile: {size: 16},
defaultTileName: "grass",
entityTypes: {
e: {
speed: 1 / 32, // in fractions of a tile per frame
color: "#00F",
size: {x:16, y:24},
pathName: "standard",
},
f: {
speed: 1 / 16, // in fractions of a tile per frame
color: "#08F",
size: {x:18, y:18},
pathName: "complex",
},
},
tileTypes: {
grass: {
style: {baseColor: "#0C3", object: false, sprite: false},
state: {passable: true}
},
wall: {
style: {baseColor: "#D35", object: false, sprite: false},
state: {passable: false}
},
},
}
const testMap = {
displayChars: {
" " : "grass", // what characters mean
"#" : "wall",
"E" : "grass", // also entity spawn
"F" : "grass", // also entity spawn
},
special: { // spawn enties and what not
"E"(idx) { entities.push(new Entity(config.entityTypes.e, idx)) },
"F"(idx) { entities.push(new Entity(config.entityTypes.f, idx)) }
},
map: // I double the width and ignor every second characters as text editors tend to make chars thinner than high
// 0_1_2_3_4_5_6_7_8_9_ x coord
"####################\n" +
"##FF ## ##\n" +
"## ## ##\n" +
"## #### ##\n" +
"## ##\n" +
"## #### ##\n" +
"## ##\n" +
"## ##\n" +
"## EE##\n" +
"####################",
// 0_1_2_3_4_5_6_7_8_9_ x coord
}
const entities = Object.assign([],{
update() {
for (const entity of entities) { entity.update() }
},
draw(ctx) {
for (const entity of entities) { entity.draw(ctx) }
},
});
const tileMap = {
map: [],
mapToIndex(x, y) { return x + y * tileMap.width },
pxToIndex(x, y) { return x / config.tile.size | 0 + (y / config.tile.size | 0) * tileMap.width },
tileByIdx(idx) { return tileMap.map[idx] },
tileByIdxDir(idx, dir) { return tileMap.map[idx + dir.x + dir.y * tileMap.width] },
idxByDir(dir) { return dir.x + dir.y * tileMap.width },
create(mapConfig) {
tileMap.length = 0;
const rows = mapConfig.map.split("\n");
tileMap.width = rows[0].length / 2 | 0;
tileMap.height = rows.length;
canvas.width = tileMap.width * config.tile.size;
canvas.height = tileMap.height * config.tile.size;
var x, y = 0;
while (y < tileMap.height) {
const row = rows[y];
for (x = 0; x < tileMap.width; x += 1) {
const char = row[x * 2];
tileMap.map.push(new Tile(mapConfig.displayChars[char], x, y));
if (mapConfig.special[char]) {
mapConfig.special[char](tileMap.mapToIndex(x, y));
}
}
y++;
}
},
update () {}, // stub
draw(ctx) {
for (const tile of tileMap.map) { tile.draw(ctx) }
},
};
function Tile(typeName, x, y) {
typeName = config.tileTypes[typeName] ? typeName : config.defaultTileName;
const t = config.tileTypes[typeName];
this.idx = x + y * tileMap.width;
this.coord = point(x * config.tile.size, y * config.tile.size);
this.style = {...t.style};
this.state = {...t.state};
}
Tile.prototype = {
draw(ctx) {
ctx.fillStyle = this.style.baseColor;
ctx.fillRect(this.coord.x, this.coord.y, config.tile.size, config.tile.size);
}
};
function Path(pathName) {
if (typeof config.pathTypes[pathName] === "string") {
config.pathTypes[pathName] = dirs.strToDirIdxs(config.pathTypes[pathName]);
}
this.indexes = config.pathTypes[pathName];
this.current = -1;
}
Path.prototype = {
nextDir(tileIdx) {
var len = this.indexes.length;
while (len--) { // make sure we dont loop forever
const dirIdx = this.indexes[this.current];
if (dirIdx > - 1) {
const canMove = tileMap.tileByIdxDir(tileIdx, dirs[dirIdx]).state.passable;
if (canMove) { return dirs[dirIdx] }
}
this.current = (this.current + 1) % this.indexes.length;
}
}
};
function Entity(type, tileIdx) {
this.coord = point();
this.move = point();
this.color = type.color;
this.speed = type.speed;
this.size = {...type.size};
this.path = new Path(type.pathName);
this.pos = this.nextTileIdx = tileIdx;
this.traveled = 1; // unit dist between tiles 1 forces update to find next direction
}
Entity.prototype = {
set dir(dir) {
if (dir === undefined) { // dont move
this.move.y = this.move.x = 0;
this.nextTileIdx = this.tileIdx;
} else {
this.move.x = dir.x * config.tile.size;
this.move.y = dir.y * config.tile.size;
this.nextTileIdx = this.tileIdx + tileMap.idxByDir(dir);
}
},
set pos(tileIdx) {
this.tileIdx = tileIdx;
const tile = tileMap.map[tileIdx];
this.coord.x = tile.coord.x + config.tile.size / 2;
this.coord.y = tile.coord.y + config.tile.size / 2;
this.traveled = 0;
},
draw(ctx) {
const ox = this.move.x * this.traveled;
const oy = this.move.y * this.traveled;
ctx.fillStyle = this.color;
ctx.fillRect(ox + this.coord.x - this.size.x / 2, oy + this.coord.y - this.size.y / 2, this.size.x, this.size.y)
},
update(){
this.traveled += this.speed;
if (this.traveled >= 1) {
this.pos = this.nextTileIdx;
this.dir = this.path.nextDir(this.tileIdx);
}
}
};
const game = {
currentStateName: undefined,
currentState: undefined,
set state(str) {
if (game.states[str]) {
if (game.currentState && game.currentState.end) { game.currentState.end() }
game.currentStateName = str;
game.currentState = game.states[str];
if (game.currentState.start) { game.currentState.start() }
}
},
states: {
setup: {
start() {
tileMap.create(testMap);
game.state = "play";
},
end() {
requestAnimationFrame(game.render); // start the render loop
delete game.states.setup; // MAKE SURE THIS STATE never happens again
},
},
play: {
render(ctx) {
tileMap.update();
entities.update();
tileMap.draw(ctx);
entities.draw(ctx);
}
}
},
renderTo: ctx,
startTime: undefined,
time: 0,
render(time) {
if (game.startTime === undefined) { game.startTime = time }
game.time = time - game.startTime;
if (game.currentState && game.currentState.render) { game.currentState.render(game.renderTo) }
requestAnimationFrame(game.render);
}
};
body{
background-color: #000;
}
canvas{
image-rendering: pixelated;
position: absolute;
top: 0;
left: 0;
width: 400px;
height: 400px;
}
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
Please Note that there are some running states that have not been tested and as such may have a typo.
Also the tile map must be walled to contain entities or they will throw when they try to leave the playfield.
The code is designed to run in the snippet. To make it work in a standard page add above the very first line setTimeout(() => game.state = "setup", 0); the line addEventListener(load", () = { and after the very last line add the line });
I found this code on the internet. I pasted into my notepad++ and then changed these two files to match the ones that I created on my desktop.
<script src='C:/Users/home-1/Desktop/Box2dWeb-2.1.a.3.js'></script>
<script src='C:/Users/home-1/Desktop/example5.js'></script>
I have the script tag in the html file but I have tried it in the code and in the file and neither of them seem to work.
For some reason the page does not work. I would like to know what is different with this page and why it does not work.
Box2dWeb-2.1.a.3.js
and then created the example5.js file and have it also on my desktop. This should show a canvas along with several objects that you can drop and drag on the screen.
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mouse Joint</title>
<script src='C:/Users/home-1/Desktop/Box2dWeb-2.1.a.3.js'></script>
<script src='C:/Users/home-1/Desktop/example5.js'></script>
<style>
canvas
{
background-color:black;
}
</style>
</head>
<body>
<canvas id='b2dCanvas' width='1024' height='500'>Broswer does not
support Canvas Tag</canvas>
<script>
(function() {
var b2Vec2 = Box2D.Common.Math.b2Vec2;
var b2BodyDef = Box2D.Dynamics.b2BodyDef;
var b2Body = Box2D.Dynamics.b2Body;
var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
var b2Fixture = Box2D.Dynamics.b2Fixture;
var b2World = Box2D.Dynamics.b2World;
var b2MassData = Box2D.Collision.Shapes.b2MassData;
var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape;
var b2DebugDraw = Box2D.Dynamics.b2DebugDraw;
var Physics = window.Physics = function(element,scale) {
var gravity = new b2Vec2(0,9.8);
this.world = new b2World(gravity, true);
this.element = element;
this.context = element.getContext("2d");
this.scale = scale || 20;
this.dtRemaining = 0;
this.stepAmount = 1/60;
};
Physics.prototype.debug = function() {
this.debugDraw = new b2DebugDraw();
this.debugDraw.SetSprite(this.context);
this.debugDraw.SetDrawScale(this.scale);
this.debugDraw.SetFillAlpha(0.3);
this.debugDraw.SetLineThickness(1.0);
this.debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
this.world.SetDebugDraw(this.debugDraw);
};
Physics.prototype.step = function(dt) {
this.dtRemaining += dt;
while(this.dtRemaining > this.stepAmount) {
this.dtRemaining -= this.stepAmount;
this.world.Step(this.stepAmount,
10, // velocity iterations
10);// position iterations
}
if(this.debugDraw) {
this.world.DrawDebugData();
} else {
var obj = this.world.GetBodyList();
this.context.clearRect(0,0,this.element.width,this.element.height);
this.context.save();
this.context.scale(this.scale,this.scale);
while(obj) {
var body = obj.GetUserData();
if(body)
{
body.draw(this.context);
}
obj = obj.GetNext();
}
this.context.restore();
}
};
Physics.prototype.click = function(callback) {
var self = this;
function handleClick(e) {
e.preventDefault();
var point = {
x: (e.offsetX || e.layerX) / self.scale,
y: (e.offsetY || e.layerY) / self.scale
};
self.world.QueryPoint(function(fixture) {
callback(fixture.GetBody(),
fixture,
point);
},point);
}
this.element.addEventListener("mousedown",handleClick);
this.element.addEventListener("touchstart",handleClick);
};
Physics.prototype.dragNDrop = function() {
var self = this;
var obj = null;
var joint = null;
function calculateWorldPosition(e) {
return point = {
x: (e.offsetX || e.layerX) / self.scale,
y: (e.offsetY || e.layerY) / self.scale
};
}
this.element.addEventListener("mousedown",function(e) {
e.preventDefault();
var point = calculateWorldPosition(e);
self.world.QueryPoint(function(fixture) {
obj = fixture.GetBody().GetUserData();
},point);
});
this.element.addEventListener("mousemove",function(e) {
if(!obj) { return; }
var point = calculateWorldPosition(e);
if(!joint) {
var jointDefinition = new Box2D.Dynamics.Joints.b2MouseJointDef();
jointDefinition.bodyA = self.world.GetGroundBody();
jointDefinition.bodyB = obj.body;
jointDefinition.target.Set(point.x,point.y);
jointDefinition.maxForce = 100000;
jointDefinition.timeStep = self.stepAmount;
joint = self.world.CreateJoint(jointDefinition);
}
joint.SetTarget(new b2Vec2(point.x,point.y));
});
this.element.addEventListener("mouseup",function(e) {
obj = null;
if(joint) {
self.world.DestroyJoint(joint);
joint = null;
}
});
};
Physics.prototype.collision = function() {
this.listener = new Box2D.Dynamics.b2ContactListener();
this.listener.PostSolve = function(contact,impulse) {
var bodyA = contact.GetFixtureA().GetBody().GetUserData(),
bodyB = contact.GetFixtureB().GetBody().GetUserData();
if(bodyA.contact) { bodyA.contact(contact,impulse,true) }
if(bodyB.contact) { bodyB.contact(contact,impulse,false) }
};
this.world.SetContactListener(this.listener);
};
var Body = window.Body = function(physics,details) {
this.details = details = details || {};
// Create the definition
this.definition = new b2BodyDef();
// Set up the definition
for(var k in this.definitionDefaults) {
this.definition[k] = details[k] || this.definitionDefaults[k];
}
this.definition.position = new b2Vec2(details.x || 0, details.y || 0);
this.definition.linearVelocity = new b2Vec2(details.vx || 0, details.vy || 0);
this.definition.userData = this;
this.definition.type = details.type == "static" ? b2Body.b2_staticBody :
b2Body.b2_dynamicBody;
// Create the Body
this.body = physics.world.CreateBody(this.definition);
// Create the fixture
this.fixtureDef = new b2FixtureDef();
for(var l in this.fixtureDefaults) {
this.fixtureDef[l] = details[l] || this.fixtureDefaults[l];
}
details.shape = details.shape || this.defaults.shape;
switch(details.shape) {
case "circle":
details.radius = details.radius || this.defaults.radius;
this.fixtureDef.shape = new b2CircleShape(details.radius);
break;
case "polygon":
this.fixtureDef.shape = new b2PolygonShape();
this.fixtureDef.shape.SetAsArray(details.points,details.points.length);
break;
case "block":
default:
details.width = details.width || this.defaults.width;
details.height = details.height || this.defaults.height;
this.fixtureDef.shape = new b2PolygonShape();
this.fixtureDef.shape.SetAsBox(details.width/2,
details.height/2);
break;
}
this.body.CreateFixture(this.fixtureDef);
};
Body.prototype.defaults = {
shape: "block",
width: 4,
height: 4,
radius: 1
};
Body.prototype.fixtureDefaults = {
density: 2,
friction: 1,
restitution: 0.2
};
Body.prototype.definitionDefaults = {
active: true,
allowSleep: true,
angle: 0,
angularVelocity: 0,
awake: true,
bullet: false,
fixedRotation: false
};
Body.prototype.draw = function(context) {
var pos = this.body.GetPosition(),
angle = this.body.GetAngle();
context.save();
context.translate(pos.x,pos.y);
context.rotate(angle);
if(this.details.color) {
context.fillStyle = this.details.color;
switch(this.details.shape) {
case "circle":
context.beginPath();
context.arc(0,0,this.details.radius,0,Math.PI*2);
context.fill();
break;
case "polygon":
var points = this.details.points;
context.beginPath();
context.moveTo(points[0].x,points[0].y);
for(var i=1;i<points.length;i++) {
context.lineTo(points[i].x,points[i].y);
}
context.fill();
break;
case "block":
context.fillRect(-this.details.width/2,
-this.details.height/2,
this.details.width,
this.details.height);
default:
break;
}
}
if(this.details.image) {
context.drawImage(this.details.image,
-this.details.width/2,
-this.details.height/2,
this.details.width,
this.details.height);
}
context.restore();
}
var physics,
lastFrame = new Date().getTime();
window.gameLoop = function() {
var tm = new Date().getTime();
requestAnimationFrame(gameLoop);
var dt = (tm - lastFrame) / 1000;
if(dt > 1/15) { dt = 1/15; }
physics.step(dt);
lastFrame = tm;
};
function init() {
var img = new Image();
// Wait for the image to load
img.addEventListener("load", function() {
physics = window.physics = new Physics(document.getElementById("b2dCanvas"));
physics.collision();
// Create some walls
new Body(physics, { color: "red", type: "static", x: 0, y: 0, height: 50, width: 0.5 });
new Body(physics, { color: "red", type: "static", x:51, y: 0, height: 50, width: 0.5});
new Body(physics, { color: "red", type: "static", x: 0, y: 0, height: 0.5, width: 120 });
new Body(physics, { color: "red", type: "static", x: 0, y:25, height: 0.5, width: 120 });
new Body(physics, { image: img, x: 5, y: 8 });
new Body(physics, { image: img, x: 13, y: 8 });
new Body(physics, { color: "blue", x: 8, y: 3 });
new Body(physics, { color: "gray", shape: "circle", radius: 4, x: 5, y: 20 });
new Body(physics, { color: "pink", shape: "polygon",
points: [ { x: 0, y: 0 }, { x: 0, y: 4 },{ x: -10, y: 0 } ],
x: 20, y: 5 });
physics.dragNDrop();
requestAnimationFrame(gameLoop);
});
img.src = "images/bricks.jpg";
}
window.addEventListener("load",init);
}());
// Lastly, add in the `requestAnimationFrame` shim, if necessary. Does nothing
// if `requestAnimationFrame` is already on the `window` object.
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame =
window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
}());
</script>
</body>
</html>
Below is what the code is supposed to produce on the screen. I was wanting to use this one as an example but have not been able to get it to work.
This is how I have the files currently:
<script src='Box2dWeb-2.1.a.3.js'></script>
<script src='example5.js'></script>
You need a file:/// prefix for both of them, e.g. file:///C:\Users\home-1\Desktop\example5.js; I’d just use a relative path, though.
Assuming the file is on your desktop too,
<script src="Box2dWeb-2.1.a.3.js"></script>
<script src="example5.js"></script>
Much more portable.
I am trying to recreate the game http://www.sinuousgame.com/ and started studying html5 canvas and kineticJS.
Recently i came across the getIntersection function and coudnt find much details regarding it.But with what i had ,i did make a code to get the Collision detection done using getIntersection() function.
But it doesnt seem to be working.
As you can see, My Fiddle: http://jsfiddle.net/p9fnq/8/
//The working player code
var LimitedArray = function(upperLimit) {
var storage = [];
// default limit on length if none/invalid supplied;
upperLimit = +upperLimit > 0 ? upperLimit : 100;
this.push = function(item) {
storage.push(item);
if (storage.length > upperLimit) {
storage.shift();
}
return storage.length;
};
this.get = function(flag) {
return storage[flag];
};
this.iterateItems = function(iterator) {
var flag, l = storage.length;
if (typeof iterator !== 'function') {
return;
}
for (flag = 0; flag < l; flag++) {
iterator(storage[flag]);
}
};
};
var tail = new LimitedArray(50);
var flag = 0, jincr = 0;
var stage = new Kinetic.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
listening: true
});
var layer = new Kinetic.Layer({
listening: true
});
stage.add(layer);
var player = new Kinetic.Circle({
x: 20,
y: 20,
radius: 6,
fill: 'cyan',
stroke: 'black',
draggable: true
});
var line = new Kinetic.Line({
points: [],
stroke: 'cyan',
strokeWidth: 2,
lineCap: 'round',
lineJoin: 'round'
});
layer.add(line);
layer.add(player);
// move the circle with the mouse
stage.getContent().addEventListener('mousemove', function() {
player.position(stage.getPointerPosition());
var obj = {
x: stage.getPointerPosition().x,
y: stage.getPointerPosition().y
};
tail.push(obj);
var arr = [];
tail.iterateItems(function(p) {
arr.push(p.x, p.y);
});
line.points(arr);
});
var x = 0;
var y = 0;
var noOfEnemies = 200;
var enemyArmada = new Array();
createEnemy();
function createEnemy() {
for (var i = 0; i < noOfEnemies; i++) {
var enemy = new Kinetic.Circle({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: 4.5 + 1.5 * Math.random(),
fill: 'red',
stroke: 'black'
});
enemy.speedX = enemy.speedY = (0.5 + Math.random() * 50);
enemyArmada.push(enemy);
layer.add(enemy);
}
}
var checkCollide = function() {
var position = stage.getPointerPosition();
if(position == null)
position = player.position();
if(position == null)
position = {x:0,y:0};
var collided = stage.getIntersection(position);
console.log(position);
if (typeof collided !== 'Kinetic.Shape') {
console.log("not shape");
}
else {
console.log("BOOOM!!!");
}
};
var anim = new Kinetic.Animation(function(frame) {
checkCollide();
for (var i = 0; i < noOfEnemies; i++) {
var e = enemyArmada[i];
e.position({
x: e.position().x - e.speedX * (frame.timeDiff / 400),
y: e.position().y + e.speedY * (frame.timeDiff / 400)
});
if (e.position().y < 0 || e.position().x < 0) {
e.position({
x: (Math.random() * (window.innerWidth + 600)),
y: -(Math.random() * window.innerHeight)
});
}
}
}, layer);
anim.start();
I need the collision to be detected. The function i have written here is checkCollide and its called within the kinetic.Animation function.
Can anyone help me out with this??
(If you don't know the solution,please do like the post,i need the solution badly)
The source of the problem
getIntersection(point) means "is any object at this point".
Since the point you're using is the player's position, getIntersection will always return true because player is always at its own position !
One solution
Put your player on one layer and all enemies on a separate layer.
That way you can hit test the enemy layer without the interference of the player object.
Code and a Demo: http://jsfiddle.net/m1erickson/JCfW8/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Prototype</title>
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v5.0.1.min.js"></script>
<style>
body{padding:20px;}
#container{
border:solid 1px #ccc;
margin-top: 10px;
width:350px;
height:350px;
}
</style>
<script>
$(function(){
var stage = new Kinetic.Stage({
container: 'container',
width: 350,
height: 350
});
var enemyLayer = new Kinetic.Layer();
stage.add(enemyLayer);
var playerLayer = new Kinetic.Layer();
stage.add(playerLayer);
var player = new Kinetic.Circle({
x:100,
y:100,
radius: 10,
fill: 'green',
draggable: true
});
player.on("dragmove",function(){
if(enemyLayer.getIntersection(player.position())){
this.fill("red");
playerLayer.draw();
}
});
playerLayer.add(player);
playerLayer.draw();
var enemy = new Kinetic.Circle({
x:200,
y:100,
radius: 20,
fill: 'blue',
draggable: true
});
enemyLayer.add(enemy);
enemyLayer.draw();
}); // end $(function(){});
</script>
</head>
<body>
<h4>Drag the green player<br>Player will turn red if it collides<br>with the blue enemy</h4>
<div id="container"></div>
</body>
</html>
Another solution
Mathematically test the player against every enemy:
Warning: untested code--some tweaking might be required
function playerEnemyCollide(){
var playerX=player.x();
var playerY=player.y();
var playerRadius=player.radius();
for(var i=0;i<enemyArmada.length;i++){
var e=enemyArmada[i];
if(circlesColliding(playerX,playerY,playerRadius,e.x,e.y,e.radius)){
return(true);
}
}
return(false);
}
function circlesColliding(cx1,cy1,radius1,cx2,cy2,radius2){
var dx=cx2-cx1;
var dy=cy2-cy1;
return(dx*dx+dy*dy<(radius1*2+radius2*2);
}
I need to allow user to rotate bitmap features on map with OpenLayers.Control.ModifyFeature or another way as it work for Polygons or other geometry objects, except Point, but only for Point I can set "externalGraphic" with my bitmap. Example of ModifyFeature to rotation as I expected here: ModifyFeature example
When I add Vector with Point geometry and activate ModifyFeature there is no rotation tool showing - only drag-drop. I know what is a point, but I need to have tool for rotate bitmap features. It may be image on any another geometry object, but with custom image.
After long research I found an example in gis.stackexchange.com, and fix it.
Here is a code which works for me:
OpenLayers.Control.RotateGraphicFeature = OpenLayers.Class(OpenLayers.Control.ModifyFeature, {
rotateHandleStyle: null,
initialize: function(layer, options) {
OpenLayers.Control.ModifyFeature.prototype.initialize.apply(this, arguments);
this.mode = OpenLayers.Control.ModifyFeature.ROTATE; // This control can only be used to rotate the feature
this.geometryTypes = ['OpenLayers.Geometry.Point'] // This control can only be used to rotate point because the 'exteralGraphic' is a point style property
var init_style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style.select);
this.rotateHandleStyle = OpenLayers.Util.extend(init_style, {
externalGraphic: "./static/resources/images/cross.png",
graphicWidth: 12,
graphicHeight: 12,
fillOpacity: 1
});
},
resetVertices: function() {
// You need to set yours renderIntent or use "vertex"
if (this.feature && this.feature.renderIntent == "vertex") {
var vertex = this.feature;
this.feature = this.backup_feature;
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
this.collectRadiusHandle();
return;
}
if (this.dragControl.feature) {
this.dragControl.outFeature(this.dragControl.feature);
}
if (this.vertices.length > 0) {
this.layer.removeFeatures(this.vertices, {silent: true});
this.vertices = [];
}
if (this.virtualVertices.length > 0) {
this.layer.removeFeatures(this.virtualVertices, {silent: true});
this.virtualVertices = [];
}
if (this.dragHandle) {
this.layer.destroyFeatures([this.dragHandle], {silent: true});
this.dragHandle = null;
}
if (this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
this.radiusHandle = null;
}
if (this.feature && this.feature.geometry &&
this.feature.geometry.CLASS_NAME != "OpenLayers.Geometry.Point") {
if ((this.mode & OpenLayers.Control.ModifyFeature.DRAG)) {
this.collectDragHandle();
}
if ((this.mode & (OpenLayers.Control.ModifyFeature.ROTATE |
OpenLayers.Control.ModifyFeature.RESIZE))) {
this.collectRadiusHandle();
}
if (this.mode & OpenLayers.Control.ModifyFeature.RESHAPE) {
if (!(this.mode & OpenLayers.Control.ModifyFeature.RESIZE)) {
this.collectVertices();
}
}
}
this.collectRadiusHandle();
},
collectRadiusHandle: function() {
var scope = this,
feature = this.feature,
geometry = this.feature.geometry || this.backup_feature.geometry,
centroid = geometry.getCentroid().transform(this.WGS84_google_mercator, this.WGS84),
lon = centroid.x, lat = centroid.y;
if (this.feature.geometry) {
this.backup_feature = this.feature;
} else {
this.feature.geometry = this.backup_feature.geometry;
}
var originGeometry = new OpenLayers.Geometry.Point(lon, lat);
// radius geometry position.
var pixel_dis_x = 10,
pixel_dis_y = -10;
var rotationFeatureGeometry = new OpenLayers.Geometry.Point(lon+pixel_dis_x, lat+pixel_dis_y);
var rotationFeature = new OpenLayers.Feature.Vector(rotationFeatureGeometry, null, this.rotateHandleStyle);
var resize = (this.mode & OpenLayers.Control.ModifyFeature.RESIZE);
var reshape = (this.mode & OpenLayers.Control.ModifyFeature.RESHAPE);
var rotate = (this.mode & OpenLayers.Control.ModifyFeature.ROTATE);
rotationFeatureGeometry.move = function(x, y) {
OpenLayers.Geometry.Point.prototype.move.call(this, x, y);
var dx1 = this.x - originGeometry.x;
var dy1 = this.y - originGeometry.y;
var dx0 = dx1 - x;
var dy0 = dy1 - y;
if (rotate) {
var a0 = Math.atan2(dy0, dx0);
var a1 = Math.atan2(dy1, dx1);
var angle = a1 - a0;
angle *= 180 / Math.PI;
var old_angle = feature.attributes.angle;
var new_angle = old_angle - angle;
feature.attributes.angle = new_angle;
// redraw the feature
scope.feature.layer.redraw.call(scope.feature.layer);
}
};
rotationFeature._sketch = true;
this.radiusHandle = rotationFeature;
this.radiusHandle.renderIntent = this.vertexRenderIntent;
this.layer.addFeatures([this.radiusHandle], {silent: true});
},
CLASS_NAME: "OpenLayers.Control.RotateGraphicFeature"
});
Style of your Vector may be as:
new OpenLayers.StyleMap({
"default": new OpenLayers.Style({
externalGraphic: "link/to/icon",
graphicHeight: "32px",
graphicWidth: "25px",
fillOpacity: 1,
rotation: "${angle}",
graphicZIndex: 1
})
})
UPD: I fixed it for OpenLayers 2.13.1
OpenLayers.Control.RotateGraphicFeature = OpenLayers.Class(OpenLayers.Control.ModifyFeature, {
rotateHandleStyle: null,
initialize: function (layer, options) {
OpenLayers.Control.ModifyFeature.prototype.initialize.apply(this, arguments);
this.mode = OpenLayers.Control.ModifyFeature.ROTATE; // This control can only be used to rotate the feature
this.geometryTypes = ['OpenLayers.Geometry.Point'] // This control can only be used to rotate point because the 'exteralGraphic' is a point style property
var init_style = OpenLayers.Util.extend({}, OpenLayers.Feature.Vector.style.select);
this.rotateHandleStyle = OpenLayers.Util.extend(init_style, {
externalGraphic: "./static/resources/images/cross.png",
graphicWidth: 12,
graphicHeight: 12,
fillOpacity: 1
});
},
resetVertices: function () {
// You need to set yours renderIntent or use "vertex"
if (this.feature && this.feature.renderIntent == "vertex") {
var vertex = this.feature;
this.feature = this.backup_feature;
if (this.dragControl.feature) {
this.dragControl.outFeature(this.dragControl.feature);
}
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
delete this.radiusHandle;
this.collectRadiusHandle();
return;
}
if (this.vertices.length > 0) {
this.layer.removeFeatures(this.vertices, {silent: true});
this.vertices = [];
}
if (this.virtualVertices.length > 0) {
this.layer.removeFeatures(this.virtualVertices, {silent: true});
this.virtualVertices = [];
}
if (this.dragHandle) {
this.layer.destroyFeatures([this.dragHandle], {silent: true});
this.dragHandle = null;
}
if (this.radiusHandle) {
this.layer.destroyFeatures([this.radiusHandle], {silent: true});
this.radiusHandle = null;
}
if (this.feature && this.feature.geometry &&
this.feature.geometry.CLASS_NAME != "OpenLayers.Geometry.Point") {
if ((this.mode & OpenLayers.Control.ModifyFeature.DRAG)) {
this.collectDragHandle();
}
if ((this.mode & (OpenLayers.Control.ModifyFeature.ROTATE |
OpenLayers.Control.ModifyFeature.RESIZE))) {
this.collectRadiusHandle();
}
if (this.mode & OpenLayers.Control.ModifyFeature.RESHAPE) {
if (!(this.mode & OpenLayers.Control.ModifyFeature.RESIZE)) {
this.collectVertices();
}
}
}
this.collectRadiusHandle();
},
collectRadiusHandle: function () {
var scope = this,
feature = this.feature,
data = feature.attributes,
geometry = this.feature.geometry || this.backup_feature.geometry,
center = this.feature.geometry.bounds.getCenterLonLat();
centroid = geometry.getCentroid().transform(this.WGS84_google_mercator, this.WGS84),
lon = centroid.x, lat = centroid.y;
if (data.type && Tms.settings.roadObjectTypeSettings[data.type].NoAzimuth) {
return;
}
if (this.feature.geometry) {
this.backup_feature = this.feature;
} else {
this.feature.geometry = this.backup_feature.geometry;
}
var originGeometry = new OpenLayers.Geometry.Point(lon, lat);
var center_px = this.map.getPixelFromLonLat(center);
// you can change this two values to get best radius geometry position.
var pixel_dis_x = 20,
pixel_dis_y = 20;
var radius_px = center_px.add(pixel_dis_x, pixel_dis_y);
var rotation_lonlat = this.map.getLonLatFromPixel(radius_px);
var rotationFeatureGeometry = new OpenLayers.Geometry.Point(
rotation_lonlat.lon, rotation_lonlat.lat
);
var rotationFeature = new OpenLayers.Feature.Vector(rotationFeatureGeometry, null, this.rotateHandleStyle);
var resize = (this.mode & OpenLayers.Control.ModifyFeature.RESIZE);
var reshape = (this.mode & OpenLayers.Control.ModifyFeature.RESHAPE);
var rotate = (this.mode & OpenLayers.Control.ModifyFeature.ROTATE);
rotationFeatureGeometry.move = function(x, y) {
OpenLayers.Geometry.Point.prototype.move.call(this, x, y);
var dx1 = this.x - originGeometry.x;
var dy1 = this.y - originGeometry.y;
var dx0 = dx1 - x;
var dy0 = dy1 - y;
if (rotate) {
var a0 = Math.atan2(dy0, dx0);
var a1 = Math.atan2(dy1, dx1);
var angle = a1 - a0;
angle *= 180 / Math.PI;
var old_angle = feature.attributes.angle;
var new_angle = old_angle - angle;
feature.attributes.angle = new_angle;
// redraw the feature
scope.feature.layer.redraw.call(scope.feature.layer);
}
};
rotationFeature._sketch = true;
this.radiusHandle = rotationFeature;
this.radiusHandle.renderIntent = this.vertexRenderIntent;
this.layer.addFeatures([this.radiusHandle], {silent: true});
},
CLASS_NAME: "OpenLayers.Control.RotateGraphicFeature"
});
I want to draw a polygon around a polyline. The polyline in my case is a Google Maps direction and I need to show a polygon around it within the Google Maps canvas.
First:
For offsetting I use the JavaScript Clipper Library. I have the following polyline (route): I make an offset polygon below using Clipper:
I have a working JS Bin example.
The code is:
<html>
<head>
<title>Javascript Clipper Library / Offset polyline</title>
<script src="clipper.js"></script>
<script>
function draw() {
var polygons = [[{"X":72,"Y":59.45},{"X":136,"Y":66},{"X":170,"Y":99},{"X":171,"Y":114},{"X":183,"Y":125},{"X":218,"Y":144},{"X":218,"Y":165},{"X":226,"Y":193},{"X":254,"Y":195},{"X":283,"Y":195},{"X":292,"Y":202},{"X":325,"Y":213},{"X":341,"Y":234},{"X":397,"Y":245},{"X":417,"Y":248}]];
var scale = 100;
reverse_copy(polygons);
polygons = scaleup(polygons, scale);
var cpr = new ClipperLib.Clipper();
var delta = 25;
var joinType = ClipperLib.JoinType.jtRound;
var miterLimit = 2;
var AutoFix = true;
var svg, offsetted_polygon,
cont = document.getElementById('svgcontainer');
offsetted_polygon = cpr.OffsetPolygons(polygons, delta * scale, joinType, miterLimit, AutoFix);
//console.log(JSON.stringify(offsetted_polygon));
// Draw red offset polygon
svg = '<svg style="margin-top:10px;margin-right:10px;margin-bottom:10px;background-color:#dddddd" width="540" height="340">';
svg += '<path stroke="red" fill="red" stroke-width="2" stroke-opacity="0.6" fill-opacity="0.2" d="' + polys2path(offsetted_polygon, scale) + '"/>';
//Draw blue polyline
svg += '<path stroke="blue" stroke-width="3" d="' + polys2path(polygons, scale) + '"/>';
svg += '</svg>';
cont.innerHTML += svg;
}
// helper function to scale up polygon coordinates
function scaleup(poly, scale) {
var i, j;
if (!scale)
scale = 1;
for(i = 0; i < poly.length; i++) {
for(j = 0; j < poly[i].length; j++) {
poly[i][j].X *= scale;
poly[i][j].Y *= scale;
}
}
return poly;
}
// converts polygons to SVG path string
function polys2path (poly, scale) {
var path = "", i, j;
if (!scale)
scale = 1;
for(i = 0; i < poly.length; i++) {
for(j = 0; j < poly[i].length; j++) {
if (!j)
path += "M";
else
path += "L";
path += (poly[i][j].X / scale) + ", " + (poly[i][j].Y / scale);
}
path += "Z";
}
return path;
}
function reverse_copy(poly) {
// Make reverse copy of polygons = convert polyline to a 'flat' polygon ...
var k, klen = poly.length, len, j;
for (k = 0; k < klen; k++) {
len = poly[k].length;
poly[k].length = len * 2 - 2;
for (j = 1; j <= len - 2; j++) {
poly[k][len - 1 + j] = {
X: poly[k][len - 1 - j].X,
Y: poly[k][len - 1 - j].Y
}
}
}
}
</script>
</head>
<body onload="draw()">
<h2>Javascript Clipper Library / Offset polyline</h2>
This page shows an example of offsetting polyline and drawing it using SVG.
<div id="svgcontainer"></div>
</body>
</html>
And all this is good but now I must replace the polygon variables with points from Google Maps directions, so I do this change:
directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
directionsDisplay.setDirections(response);
function draw() {
var polygons = response.routes[0].overview_path;
//REST OF CODE
}
}
}
I have a JS Bin example with this code for offsetting the polygon around the polyline.
But there is some problem, which I can't regonize and I can't get a polygon around directions.
Is there any way to solve this problem?
My working solution: working example (based off of Manolis Xountasis's answer) and pieces from these related questions:
How to calculate intersection area in Google Maps API with JSTS Library?
Google Maps Polygons self intersecting detection
include the JSTS library
add routines to translate google.maps.Polyline paths to JSTS objects:
function googleMaps2JTS(boundaries) {
var coordinates = [];
var length = 0;
if (boundaries && boundaries.getLength) length = boundaries.getLength();
else if (boundaries && boundaries.length) length = boundaries.length;
for (var i = 0; i < length; i++) {
if (boundaries.getLength) coordinates.push(new jsts.geom.Coordinate(
boundaries.getAt(i).lat(), boundaries.getAt(i).lng()));
else if (boundaries.length) coordinates.push(new jsts.geom.Coordinate(
boundaries[i].lat(), boundaries[i].lng()));
}
return coordinates;
};
and back to google.maps.LatLng arrays
var jsts2googleMaps = function (geometry) {
var coordArray = geometry.getCoordinates();
GMcoords = [];
for (var i = 0; i < coordArray.length; i++) {
GMcoords.push(new google.maps.LatLng(coordArray[i].x, coordArray[i].y));
}
return GMcoords;
}
get the directions polyline from the DirectionsService and buffer it
directionsService.route(request, function (response, status) {
if (status == google.maps.DirectionsStatus.OK) {
directionsDisplay.setDirections(response);
var overviewPath = response.routes[0].overview_path,
overviewPathGeo = [];
for (var i = 0; i < overviewPath.length; i++) {
overviewPathGeo.push(
[overviewPath[i].lng(), overviewPath[i].lat()]);
}
var distance = 10 / 111.12, // Roughly 10km
geoInput = {
type: "LineString",
coordinates: overviewPathGeo
};
var geoInput = googleMaps2JTS(overviewPath);
var geometryFactory = new jsts.geom.GeometryFactory();
var shell = geometryFactory.createLineString(geoInput);
var polygon = shell.buffer(distance);
var oLanLng = [];
var oCoordinates;
oCoordinates = polygon.shell.points[0];
for (i = 0; i < oCoordinates.length; i++) {
var oItem;
oItem = oCoordinates[i];
oLanLng.push(new google.maps.LatLng(oItem[1], oItem[0]));
}
if (routePolygon && routePolygon.setMap) routePolygon.setMap(null);
routePolygon = new google.maps.Polygon({
paths: jsts2googleMaps(polygon),
map: map
});
}
});
This is the working solution. You can find the JSTS files at coderwall.com.
var overviewPath = response.routes[0].overview_path,
overviewPathGeo = [];
for (var i = 0; i < overviewPath.length; i++) {
overviewPathGeo.push(
[overviewPath[i].lng(), overviewPath[i].lat()]
);
}
var distance = value / 10000, // Roughly 10km
geoInput = {
type: "LineString",
coordinates: overviewPathGeo
};
var geoReader = new jsts.io.GeoJSONReader(),
geoWriter = new jsts.io.GeoJSONWriter();
var geometry = geoReader.read(geoInput).buffer(distance);
var polygon = geoWriter.write(geometry);
var oLanLng = [];
var oCoordinates;
oCoordinates = polygon.coordinates[0];
for (i = 0; i < oCoordinates.length; i++) {
var oItem;
oItem = oCoordinates[i];
oLanLng.push(new google.maps.LatLng(oItem[1], oItem[0]));
}
var polygone = new google.maps.Polygon({
paths: oLanLng,
map:map
});
This is an alternate solution using Turf.js's buffer module. I have used a Leaflet map to demonstrate the results - but this will work for any mapping library.
var center = [37.78791180770003, -122.40962505340575];
var map = L.map('map').setView(center, 14);;
L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(map);
var line = {
"type": "Feature",
"properties": {
"color": "blue"
},
"geometry": {
"type": "LineString",
"coordinates": [
[-122.40447521209718,
37.79367718768535
],
[-122.40803718566895,
37.79171022624846
],
[-122.40769386291502,
37.79096412372944
],
[-122.40662097930908,
37.789641468930114
],
[-122.40941047668457,
37.789675383451495
],
[-122.40992546081543,
37.78875968591083
],
[-122.40962505340575,
37.78791180770003
]
]
}
};
L.geoJSON(line, {
style: function(feature) {
return {
color: feature.properties.color
};
}
}).addTo(map);
var polygon = turf.buffer(line, 50, {
units: 'meters'
});
L.geoJSON(polygon, {
style: function(feature) {
return {
color: feature.properties.color
};
}
}).addTo(map);
#map {
height: 400px;
}
<script src="https://npmcdn.com/#turf/turf#6.3.0/turf.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.5.1/leaflet.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.5.1/leaflet.js"></script>
<div id="map"></div>