How to programmatically draw a thick line JavaScript? - javascript

I'm trying to define racecourse with a dynamic line that user can draw on a canvas element. So when line has been drawn, program should add sidelines for it as shown in the picture below:
I have managed to mimic the idea already by using line normals but can't get it done correctly. At the moment I put point in the midway of the lines in the direction of the line normals and draw outlines using those points. While generated line is relatively smooth in cases on large turns, tight turns tend to produce loops.
As seen in image below:
Here is current code that generates points for side lines above (I'm using p5.js JavaScript library):
var sketch = function (p) {
with(p) {
let handpoints;
let walkhandpoints;
let collect;
let parsepath;
let shapse;
let step;
let tmp;
let dorender;
let lineoffset;
p.setup = function() {
createCanvas(600, 600);
handpoints = [];
walkhandpoints = 10;
collect = true;
parsepath = false;
shapes = [];
step = 2;
tmp = [];
dorender = true;
lineoffset = 15;
};
p.draw = function() {
if(dorender) {
background(220);
update();
for (let shape of shapes) {
shape.show();
}
}
};
function update() {
if (mouseIsPressed) {
if (collect) {
let mouse = createVector(mouseX, mouseY);
handpoints.push(mouse);
Shape.drawPath(handpoints);
parsepath = true;
}
} else if (parsepath) {
let tmp1 = Shape.cleanPath(handpoints, step);
let s1 = new Shape(tmp1, 1, 'line', color(175));
shapes.push(s1);
let tmp2 = Line.sidePoints(tmp1, lineoffset);
let s2 = new Shape(tmp2.sideA, 1, 'line', color(175,120,0));
let s3 = new Shape(tmp2.sideB, 1, 'line', color(175,0, 120));
shapes.push(s2);
shapes.push(s3);
handpoints = [];
parsepath = false;
//dorender = false;
}
}
class Shape {
constructor(points, mag, type = 'line', shader = color(200, 0, 100)) {
this.points = points.slice().map(item => item.copy());
this.type = type;
this.mag = mag;
this.shader = shader;
}
static cleanPath(points, step) {
let tmp = [];
let output = [];
for (let i = 1; i < points.length; i++) {
let prev = points[i - 1];
let curr = points[i];
if (!prev.equals(curr)) {
tmp.push(prev.copy())
if (i === points.length - 1) {
tmp.push(curr.copy())
}
}
}
for (let i = 0; i < tmp.length; i++) {
if(i % step === 0) {
output.push(tmp[i]);
}
}
output.push(output[0]);
return output;
}
static drawPath(points, mag = 1, type = 'line', shader = color(175)) {
let s = new Shape(points, mag, type, shader);
s.show();
}
show() {
for (let i = 0; i < this.points.length; i++) {
if (this.type === 'line' && i > 0) {
let prev = this.points[i - 1];
let curr = this.points[i];
strokeWeight(this.mag);
stroke(this.shader);
line(prev.x, prev.y, curr.x, curr.y);
} else if (this.type === 'point') {
noStroke();
fill(this.shader);
ellipse(this.points[i].x, this.points[i].y, this.mag * 2, this.mag * 2);
}
}
}
}
class Line {
static sidePoints(points, lineoffset) {
let sideA = [];
let sideB = [];
for(let i = 1; i < points.length; i++) {
// take consecutive points
let prev = points[i-1];
let curr = points[i];
// calculate normals
let dx = curr.x-prev.x;
let dy = curr.y-prev.y;
let a = createVector(-dy, dx).normalize();
let b = createVector(dy, -dx).normalize();
// calculate midway of the two points
let px = (prev.x+curr.x)/2;
let py = (prev.y+curr.y)/2;
let p = createVector(px,py);
// put created points back along drawed line
a.mult(lineoffset).add(p);
b.mult(lineoffset).add(p);
sideA.push(a);
sideB.push(b);
}
// close paths
if(!sideA[0].equals(sideA[sideA.length-1])) {
sideA.push(sideA[0]);
}
if(!sideB[0].equals(sideB[sideB.length-1])) {
sideB.push(sideB[0]);
}
return {sideA, sideB};
}
}
}
};
let node = document.createElement('div');
window.document.getElementById('p5-container').appendChild(node);
new p5(sketch, node);
body {
background-color:#ffffff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script>
<div id="p5-container"></div>
Firstly I'd like to find a way to draw those points in the corresponding corner points of the drawn line so when drawn line has only few points the outlines would retain the drawn shape.
Secondly is there some good way to reduce points on the areas where there are several of them to reduce those loops in small corners and other type errors in generated lines?
Idea is to get points for lines, so it would be easy to detect with line intersection if race car velocity vector crosses it.
Unfortunately I'm not very familiar with math notations so please try to use easy to understand version of them if there is some fancy math that would do the job.

In the future, please try to post a minimal example. I was not able to run your code as you posted it.
That being said, one option you could consider is using the strokeWeight() function to draw the path at different widths. Here's an example:
const path = [];
function setup() {
createCanvas(400, 400);
// Add some default points to the path.
path.push(createVector(0, 0));
path.push(createVector(width/4, height/4));
}
function draw() {
background(220);
// Draw the path with a thick gray line.
strokeWeight(50);
stroke(200);
for(let i = 1; i < path.length; i++){
const prevPoint = path[i-1];
const nextPoint = path[i];
line(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
}
// Draw the path with a thin black line.
strokeWeight(1);
stroke(0);
for(let i = 1; i < path.length; i++){
const prevPoint = path[i-1];
const nextPoint = path[i];
line(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
}
}
// Add a point to the path when the user clicks.
function mousePressed(){
path.push(createVector(mouseX, mouseY));
}
The trick here is to draw the path in two passes. First you draw the path using a thick line, and then you draw the path again, this time using a thin line.

Related

JavaScript - Calculate area of region drawn on canvas

I have a javascript function that calculates the area of a region drawn on a canvas, the function is extremely slow and makes the performance of the web app horrible, I was wondering if there is any way I can optimize it?
It works fine if a user triggers it once in a while, but there are scenarios where it will be rapidly triggered by the user, sometimes up to 200 times in 5 seconds, so it's not ideal.
Tegaki.refreshMeasurements = () => {
for (let layer of Tegaki.layers) {
if (layer.name == 'background') continue;
let canvas = layer.canvas
let i, ctx, dest, data, len;
ctx = canvas.getContext('2d')
dest = ctx.getImageData(0, 0, canvas.width, canvas.height);
data = dest.data,
len = data.length;
let totalPixels = 0;
let totalHU = 0
for (i = 3; i < len; i += 4) {
if (data[i] > 0) {
totalPixels++;
totalHU += (Tegaki.selectedSlicePixelData[Math.floor(i/4)] +
Tegaki.selectedSliceRescaleIntercept)
}
}
let area = totalPixels * Tegaki.selectedSlicePixelSpacing / 100;
let meanHU = totalHU / totalPixels
let layerType = _this.layerTypes.find(t => t.id == layer.name)
layerType.area = area;
layerType.meanHU = meanHU
}
}
I think the part causing the most performance drop is the for-loop because the length of the image data array sometimes is over 1 million so this thing probably loops for a while.
Is there any better way to implement this?

Javascript Atom: Array Problems

So, for a final project I'm trying to make a game with three different meteors; Bronze, Silver and Gold. While the Bronze array works fine in Setup(), the Silver and Gold meteors go at high speeds for some unknown reason.
function setup() {
createCanvas(windowWidth, windowHeight);
spaceship = new Spaceship(100, 100, 5, spaceshipImage, bulletImage, 40);
healthStar = new Star(1000, 100, 10, healthStarImage, 50);
//the Meteor Array
// Run a for loop numMeteor times to generate each meteor and put it in the array
// with random values for each star
for (let i = 0; i < numMeteor; i++) {
let meteorX = random(0, width);
let meteorY = random(0, height);
let meteorSpeed = random(2, 20);
let meteorRadius = random(10, 60);
meteor.push(new Meteor(meteorX, meteorY, meteorSpeed, meteorBronzeImage, meteorRadius));
}
}
// draw()
//
// Handles input, movement, eating, and displaying for the system's objects
function draw() {
// Set the background to a safari scene
background(skyBackground);
// Check if the game is in play
if (playing == true) {
// Handle input for the tiger
spaceship.handleInput();
// Move all the "animals"
spaceship.move();
healthStar.move();
if (spaceship.dodges >= 5){
levelTwo = true;
}
//lvl 2
if (levelTwo == true){
meteor = [];
for (let i = 0; i < numMeteor; i++) {
let meteorX = random(0, width);
let meteorY = random(0, height);
let meteorSpeed = random(2, 20);
let meteorRadius = random(10, 60);
meteor.push(new Meteor(meteorX, meteorY, meteorSpeed, meteorSilverImage, meteorRadius));
}
}
if (spaceship.dodges >= 8){
levelThree = true;
}
//lvl 3
if (levelThree == true){
levelTwo = false;
meteor = [];
for (let i = 0; i < numMeteor; i++) {
let meteorX = random(0, width);
let meteorY = random(0, height);
let meteorSpeed = random(2, 20);
let meteorRadius = random(10, 60);
meteor.push(new Meteor(meteorX, meteorY, meteorSpeed, meteorGoldImage, meteorRadius));
}
}
// Handle the tiger and lion eating any of the star
spaceship.handleEating(healthStar);
//
spaceship.handleBullets();
// Handle the tragic death of the tiger
spaceship.handleDeath();
// Check to see when the game is over
checkGameOver();
// Display all the "animals"
spaceship.display();
healthStar.display();
// Display and making sure the tiger can eat the copies of the star
for (let i = 0; i < meteor.length; i++) {
meteor[i].move();
meteor[i].display();
//meteor[i].handleDamage();
spaceship.handleHurting(meteor[i]);
spaceship.handleDodging(meteor[i]);
}
}
// Once the game is over, display a Game Over Message
if (gameOver == true) {
displayGameOver();
}
// Otherwise we display the message to start the game
else {
displayStartMessage();
}
}
I've tried to change the speeds, made the levels false, nothing's working other than the Bronze meteors.
Your level 2 and level 3 meteor initialization is inside your draw loop. They include a meteor = [] statement. From what you've provided, that suggests your meteor array is getting cleared every single draw iteration. They never have a chance to move, you're getting fresh random meteors each time.
If the array clearing within your draw loop is in fact the issue, you'll need to add a way to track if the level initialization has been completed, so that it only occurs once. A simple flag, an idempotent function, something like that.
// Extract meteor generation to it's own function so you dont need to repeat it
function generateMeteors(meteorImage) {
let meteor = [];
for (let i = 0; i < numMeteor; i++) {
let meteorX = random(0, width);
let meteorY = random(0, height);
let meteorSpeed = random(2, 20);
let meteorRadius = random(10, 60);
meteor.push(new Meteor(meteorX, meteorY, meteorSpeed, meteorBronzeImage, meteorRadius));
}
return meteor;
}
function setup() {
createCanvas(windowWidth, windowHeight);
spaceship = new Spaceship(100, 100, 5, spaceshipImage, bulletImage, 40);
healthStar = new Star(1000, 100, 10, healthStarImage, 50);
//the Meteor Array
// meteor is apparently global, or otherwise in scope here
meteor = generateMeteors(meteorBronzeImage)
}
function draw() {
/**
Code removed for clarity
**/
if (spaceship.dodges >= 5) {
levelTwo = true;
}
//lvl 2
// levelTwoInitialized tracks if we've initialized the meteor array for this level
if (levelTwo == true && levelTwoInitialized == false){
meteor = generateMeteors(meteorSilverImage);
levelTwoInitialized = true;
}
if (spaceship.dodges >= 8){
levelThree = true;
}
//lvl 3
// levelThreeInitialized tracks if we've initialized the meteor array for this level
if (levelThree == true && levelThreeInitialized == false){
levelTwo = false;
meteor = generateMeteors(meteorGoldImage);
levelThreeInitialized = true;
}
//... rest of code
One possible way to do this is above. Level initialization flags like levelThreeInitialized would have to be declared somewhere, wherever you're tracking your game/program state.
If you want to retain the previous level's meteors, you could do something like:
if (levelTwo == true && levelTwoInitialized == false){
// add silver meteors to the existing bronze set
meteor = meteor.concat(generateMeteors(meteorSilverImage));
levelTwoInitialized = true;
}
//... rest of code
If you want to create a new set with multiple types:
if (levelTwo == true && levelTwoInitialized == false){
// make NEW silver and bronze meteors
meteor = generateMeteors(meteorSilverImage)
.concat(generateMeteors(meteorBronzeImage);
levelTwoInitialized = true;
}
//... rest of code

How to straighten unneeded turns in a A* graph search result?

I have been working on a JavaScript implementation of the early 90's adventure games and specifically plotting a path from the place the hero is standing to the location the player has clicked on. My approach is to first determine if a strait line (without obstructions) can be drawn, if not then to search for a path of clear way-points using Brian Grinstead's excellent javascript-astar. The problem I'm facing however is the path (while optimal will veer into spaces that would seem to the user an unintended. Here is a classic example of what I'm talking about (the green path is the generated path, the red dots are each turns where the direction of the path changes):
Now I know that A* is only guaranteed to return a path that cannot be simpler (in terms of steps), but I'm struggling to implement a heuristic that weights turns. Here is a picture that show two other paths that would also qualify as just as simple (with an equal number of steps)
The Blue path would present the same number of steps and turns while the red path has the same number of steps and fewer turns. In my code I have a simplifyPath() function that removes steps where the direction changes, so if I could get all possible paths from astar then I could select the one with the least turns, but that's not how A* fundamentally works, so I'm looking for a way to incorporate simplicity into the heuristic.
Here is my current code:
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
getPosition: function(event) {
var bounds = field.getBoundingClientRect(),
x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];
return {
x: x,
y: y,
node: node
}
},
drawObstructions: function() {
context.clearRect (0, 0, 320, 200);
if(img) {
context.drawImage(img, 0, 0);
} else {
context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(200, 100, 50, 50);
context.fillRect(0, 100, 50, 50);
context.fillRect(100, 100, 50, 50);
context.fillRect(0, 50, 150, 50);
}
},
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
return simplePath;
},
drawPath: function(start, end) {
var path, step, next;
if(this.isPathClear(start, end)) {
this.drawLine(start, end);
} else {
path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
if(path.length > 1) {
step = path[0];
for(next = 1; next < path.length; next++) {
this.drawLine(step, path[next]);
step = path[next];
}
}
}
},
drawLine: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
context.fillStyle = 'rgb(255, 0, 0)';
} else {
context.fillStyle = 'rgb(0, 255, 0)';
}
context.fillRect(x, y, 1, 1);
if(x === end.x && y === end.y) {
break;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
},
isPathClear: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
return false;
}
if(x === end.x && y === end.y) {
return true;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
}
}, graph;
engine.drawObstructions();
graph = (function() {
var x, y, rows = [], cols, js = '[';
for(y = 0; y < 200; y += graphSettings.size) {
cols = [];
for(x = 0; x < 320; x += graphSettings.size) {
cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
}
js += '['+cols+'],\n';
rows.push(cols);
}
js = js.substring(0, js.length - 2);
js += ']';
document.getElementById('Graph').value=js;
return new Graph(rows, { diagonal: true });
})();
return engine;
}, start, end, engine = EngineBuilder(field, 10);
field.addEventListener('click', function(event) {
var position = engine.getPosition(event);
if(!start) {
start = position;
} else {
end = position;
}
if(start && end) {
engine.drawObstructions();
engine.drawPath(start, end);
start = end;
}
}, false);
#field {
border: thin black solid;
width: 98%;
background: #FFFFC7;
}
#Graph {
width: 98%;
height: 300px;
overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>
After digging into Michail Michailidis' excellent answer I've added the following code to my simplifyPath() function) (demo):
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1],
simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}],
i,
finalPath = [simplePath[0]],
classification,
previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
previous = simplePath[0];
for(i = 2; i < simplePath.length; i++) {
if(!this.isPathClear(previous, simplePath[i])) {
finalPath.push(simplePath[i-1]);
previous = simplePath[i-1];
}
}
finalPath.push(end);
return finalPath;
}
Basically after it reduces redundant steps in the same direction, it tries to smooth out the path by looking ahead to see if it can eliminate any steps.
Very very intresting problem! Thanks for this question! So...some observations first:
Not allowing diagonal movement fixes this problem but since you are interested in diagonal movement I had to search more.
I had a look at path simplifying algorithms like:
Ramer Douglas Peuker
(http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)
and an implementation: https://gist.github.com/rhyolight/2846020.
I added an implementation to your code without success. This algorithm doesn't take into account obstacles so it was difficult to adapt it.
I wonder what would the behavior be (for diagonal movements) if you had used Dijkstra instead of A* or if you used an 'all shortest paths between a pair of nodes' algorithm and then you sorted them by increasing changes in direction.
After reading a bit about A* here http://buildnewgames.com/astar/ I thought that the implementation of A-star you are using is the problem or the heuristics. I tried all the heuristics on the a-star of your code including euclidean that I coded myself and tried also all the heuristics in the http://buildnewgames.com/astar code Unfortunately all of the diagonal allowing heuristics were having the same issue you are describing.
I started working with their code because it is a one-to-one grid and yours was giving me issues drawing. Your simplifyPath that I tried to remove was also causing additional problems. You have to keep in mind that since
you are doing a remapping this could be an issue based on that
On a square grid that allows 4 directions of movement, use Manhattan distance (L1).
On a square grid that allows 8 directions of movement, use Diagonal distance (Lāˆž).
On a square grid that allows any direction of movement, you might or might not want Euclidean distance (L2). If A* is finding paths on the grid but you are allowing movement not on the grid, you may want to consider other representations of the map. (from http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html)
What is my pseudocode algorithm:
var path = A-star();
for each node in path {
check all following nodes till some lookahead limit
if you find two nodes in the same row but not column or in the same column but not row {
var nodesToBeStraightened = push all nodes to be "straightened"
break the loop;
}
skip loop index to the next node after zig-zag
}
if nodesToBeStraightened length is at least 3 AND
nodesToBeStraightened nodes don't form a line AND
the resulting Straight line after simplification doesn't hit an obstruction
var straightenedPath = straighten by getting the first and last elements of nodesToBeStraightened and using their coordinates accordingly
return straightenedPath;
Here is the visual explanation of what is being compared in the algorithm:
Visual Explanation:
How this code will be used with yours (I did most of the changes - I tried my best but there are so many problems like with how you do drawing and because of the rounding of the grid etc - you have to use a grid and keep the scale of the paths accurate - please see also assumptions below):
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
//[...] missing code
removeZigZag: function(currentPath,lookahead){
//for each of the squares on the path - see lookahead more squares and check if it is in the path
for (var i=0; i<currentPath.length; i++){
var toBeStraightened = [];
for (var j=i; j<lookahead+i+1 && j<currentPath.length; j++){
var startIndexToStraighten = i;
var endIndexToStraighten = i+1;
//check if the one from lookahead has the same x xor the same y with one later node in the path
//and they are not on the same line
if(
(currentPath[i].x == currentPath[j].x && currentPath[i].y != currentPath[j].y) ||
(currentPath[i].x == currentPath[j].y && currentPath[i].x != currentPath[j].x)
) {
endIndexToStraighten = j;
//now that we found something between i and j push it to be straightened
for (var k = startIndexToStraighten; k<=endIndexToStraighten; k++){
toBeStraightened.push(currentPath[k]);
}
//skip the loop forward
i = endIndexToStraighten-1;
break;
}
}
if (toBeStraightened.length>=3
&& !this.formsALine(toBeStraightened)
&& !this.lineWillGoThroughObstructions(currentPath[startIndexToStraighten], currentPath[endIndexToStraighten],this.graph?????)
){
//straightening:
this.straightenLine(currentPath, startIndexToStraighten, endIndexToStraighten);
}
}
return currentPath;
},
straightenLine: function(currentPath,fromIndex,toIndex){
for (var l=fromIndex; l<=toIndex; l++){
if (currentPath[fromIndex].x == currentPath[toIndex].x){
currentPath[l].x = currentPath[fromIndex].x;
}
else if (currentPath[fromIndex].y == currentPath[toIndex].y){
currentPath[l].y = currentPath[fromIndex].y;
}
}
},
lineWillGoThroughObstructions: function(point1, point2, graph){
var minX = Math.min(point1.x,point2.x);
var maxX = Math.max(point1.x,point2.x);
var minY = Math.min(point1.y,point2.y);
var maxY = Math.max(point1.y,point2.y);
//same row
if (point1.y == point2.y){
for (var i=minX; i<=maxX && i<graph.length; i++){
if (graph[i][point1.y] == 1){ //obstacle
return true;
}
}
}
//same column
if (point1.x == point2.x){
for (var i=minY; i<=maxY && i<graph[0].length; i++){
if (graph[point1.x][i] == 1){ //obstacle
return true;
}
}
}
return false;
},
formsALine: function(pointsArray){
//only horizontal or vertical
if (!pointsArray || (pointsArray && pointsArray.length<1)){
return false;
}
var firstY = pointsArray[0].y;
var lastY = pointsArray[pointsArray.length-1].y;
var firstX = pointsArray[0].x;
var lastX = pointsArray[pointsArray.length-1].x;
//vertical line
if (firstY == lastY){
for (var i=0; i<pointsArray.length; i++){
if (pointsArray[i].y!=firstY){
return false;
}
}
return true;
}
//horizontal line
else if (firstX == lastX){
for (var i=0; i<pointsArray.length; i++){
if (pointsArray[i].x!=firstX){
return false;
}
}
return true;
}
return false;
}
//[...] missing code
}
//[...] missing code
}
Assumptions and Incompatibilities of the above code:
obstacle is 1 and not 0
the orginal code I have in the demo is using array instead of {x: number, y:number}
in case you use the other a-star implementation, the point1 location is on the column 1 and row 2.
converted to your {x: number, y:number} but haven't checked all the parts:
I couldn't access the graph object to get the obstacles - see ?????
You have to call my removeZigZag with a lookahead e.g 7 (steps away) in the place where you were
doing your path simplification
admittedly their code is not that good compared to the a-star implementation from Stanford but for our purposes it should be irelevant
possibly the code has bugs that I don't know of and could be improved. Also I did my checks only in this specific world configuration
I believe the code has complexity O(N x lookahead)~O(N) where N is the number of steps in the input A* shortest path.
Here is the code in my github repository (you can run the demo)
https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
based on this A* Javascript implementation downloaded from here: http://buildnewgames.com/astar/
Their clickHandling and world boundary code is broken as when you click on the right side of the map the path calculation is not working sometimes. I didn't have time to find their bug. As a result my code has the same issue
Probably it is because the map I put from your question is not square - but anyway my algorithm should be unaffected. You will see this weird behavior is happening if non of my remove ZigZag code runs. (Edit: It was actually because the map was not square - I updated the map to be square for now)
Feel free to play around by uncommenting this line to see the before-after:
result = removeZigZag(result,7);
I have attached 3 before after image sets so the results can be visualized:
(Keep in mind to match start and goal if you try them - direction DOES matter ;) )
Case 1: Before
Case 1: After
Case 2: Before
Case 2: After
Case 3: Before
Case 3: After
Case 4: Before
Case 4: After
Resources:
My code (A* diagonal movement zig zag fix demo): https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
Original Javascript A* implementation of my demo can be found above (first commit) or here: - http://buildnewgames.com/astar/
A* explanation: http://buildnewgames.com/astar/
A* explanation from Stanford: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
JavaScript A* implementation used by OP's question (Github):
Ramer Douglas Peuker Algorithm (Wikipedia): http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
Javascript implementation of Douglas Peuker Algorithm: https://gist.github.com/rhyolight/2846020
A* Algorithm (Wikipedia): http://en.wikipedia.org/wiki/A*_search_algorithm
You can use a modified A* algorithm to account for changes in direction. While simplifying the result of the standard A* algorithm may yield good results, it may not be optimal. This modified A* algorithm will return a path of minimal length with the least number of turns.
In the modified A* algorithm, each position corresponds to eight different nodes, each with their own heading. For example, the position (1, 1) corresponds to the eight nodes
(1,1)-up, (1,1)-down, (1,1)-right, (1,1)-left,
(1,1)-up-left, (1,1)-up-right, (1,1)-down-left, and (1,1)-down-right
The heuristic distance from a node to the goal is the the heuristic distance from the corresponding point to the goal. In this case, you probably want to use the following function:
H(point) = max(abs(goal.xcor-point.xcor), abs(goal.ycor-point.ycor))
The nodes that correspond to a particular position are connected to the nodes of the neighboring positions with the proper heading. For example, the nodes corresponding to the position (1,1) are all connected to the following eight nodes
(1,2)-up, (1,0)-down, (2,1)-right, (0,1)-left,
(0,2)-up-left, (2,2)-up-right, (0,0)-down-left, and (2,0)-down-right
The distance between any two connected nodes depends on their heading. If they have the same head, then the distance is 1, otherwise, we have made a turn, so the distance is 1+epsilon. epsilon represents an arbitrarily small value/number.
We know need to have a special case for the both the start and goal. The start and goal are both represented as a single node. At the start, we have no heading, so the distance between the start node and any connected node is 1.
We can now run the standard A* algorithm on the modified graph. We can map the returned path to a path in the original grid, by ignoring the headings. The total length of the returned path will be of the form n+m*epsilon. n is the total length of the corresponding path in the original grid, and m is the number of turns. Because A* returns a path of minimal length, the path in the original grid is a path of minimal length that makes the least turns.
I have come up with somewhat of a fix that is a simple addition to your original code, but it doesn't work in all situations (see image below) because we are limited to what the A* returns us. You can see my jsfiddle here
I added the following code to your simplifyPath function right before the return. What it does is strips out extra steps by seeing if there is a clear path between non-adjacent steps (looking at larger gaps first). It could be optimized, but you should get the gist from what I've got.
do{
shortened = false;
loop:
for(i = 0; i < simplePath.length; i++) {
for(j = (simplePath.length - 1); j > (i + 1); j--) {
if(this.isPathClear(simplePath[i],simplePath[j])) {
simplePath.splice((i + 1),(j - i - 1));
shortened = true;
break loop;
}
}
}
} while(shortened == true);
You can see below that this removes the path that goes in on the left (as in the question) but also that not all the odd turns are removed. This solution only uses the points provided from the A*, not points in between on the path - for example, because the 2nd point does not have a straight unobstructed line to the 4th or 5th points, it cannot optimize point 3 out. It happens a lot less than the original code, but it still does give weird results sometimes.
In edition to nodes having references to their parent nodes. Also store which direction that node came from inside a variable. In my case there was only two possibilities horizontally or vertically. So I created two public static constants for each possibility. And a helper function named "toDirection" which takes two nodes and return which direction should be taken in order to go from one to another:
public class Node {
final static int HORIZONTALLY = 0;
final static int VERTICALLY = 1;
int col, row;
boolean isTravelable;
int fromDirection;
double hCost;
double gCost;
double fCost;
Node parent;
public Node(int col, int row, boolean isTravelable) {
this.col = col;
this.row = row;
this.isTravelable = isTravelable;
}
public static int toDirection(Node from, Node to) {
return (from.col != to.col) ? Node.HORIZONTALLY : Node.VERTICALLY;
}
}
Then you can change your weight calculation function to take turns into account. You can now give a small punishment for turns like:
public double calcGCost(Node current, Node neighbor) {
if(current.fromDirection == Node.toDirection(current, neighbor)) {
return 1;
} else{
return 1.2;
}
}
Full code: https://github.com/tezsezen/AStarAlgorithm
At the risk of potential down voting, I will try my best to suggest an answer. If you weren't using a third party plugin I would suggest a simple pop/push stack object be built however since you are using someone else's plugin it might be best to try and work along side it rather than against it.
That being said I might just do something simple like track my output results and try to logically determine the correct answer. I would make a simple entity type object literal for storage within an array of all possible path's? So the entire object's life span is only to hold position information. Then you could later parse that array of objects looking for the smallest turn count.
Also, since this third party plugin will do most of the work behind the scenes and doesn't seem very accessible to extract, you might need to feed it criteria on your own. For example if its adding more turns then you want, i.e. inside the door looking square, then maybe sending it the coordinates of the start and end arent enouugh. Perhaps its better to stop at each turn and send in the new coordinates to see if a straight line is now possible. If you did this then each turn would have a change to look and see if there is an obstruction stopping a straight line movement.
I feel like this answer is too simple so it must be incorrect but I will try nonetheless...
//Entity Type Object Literal
var pathsFound = function() {
//Path Stats
straightLine: false,
turnCount: 0,
xPos: -1, //Probably should not be instantiated -1 but for now it's fine
yPos: -1,
//Getters
isStraightLine: function() { return this.straightLine; },
getTurnCount: function() { return this.turnCount; },
getXPos: function() { return this.xPos; },
getYPos: function() { return this.yPos; },
//Setters
setStraightLine: function() { this.straightLine = true; },
setCrookedLine: function() { this.straightLine = false; },
setXPos: function(val) { this.xPos = val; },
setYPos: function(val) { this.yPos = val; },
//Class Functionality
incrementTurnCounter: function() { this.turnCount++; },
updateFullPosition: function(xVal, yVal) {
this.xPos = xVal;
this.yPos = yVal.
},
}
This way you could report all the data every step of the way and before you draw to the screen you could iterate through your array of these object literals and find the correct path by the lowest turnCount.
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
getPosition: function(event) {
var bounds = field.getBoundingClientRect(),
x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];
return {
x: x,
y: y,
node: node
}
},
drawObstructions: function() {
context.clearRect (0, 0, 320, 200);
if(img) {
context.drawImage(img, 0, 0);
} else {
context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(200, 100, 50, 50);
context.fillRect(0, 100, 50, 50);
context.fillRect(100, 100, 50, 50);
context.fillRect(0, 50, 150, 50);
}
},
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
return simplePath;
},
drawPath: function(start, end) {
var path, step, next;
if(this.isPathClear(start, end)) {
this.drawLine(start, end);
} else {
path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
if(path.length > 1) {
step = path[0];
for(next = 1; next < path.length; next++) {
this.drawLine(step, path[next]);
step = path[next];
}
}
}
},
drawLine: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
context.fillStyle = 'rgb(255, 0, 0)';
} else {
context.fillStyle = 'rgb(0, 255, 0)';
}
context.fillRect(x, y, 1, 1);
if(x === end.x && y === end.y) {
break;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
},
isPathClear: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
return false;
}
if(x === end.x && y === end.y) {
return true;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
}
}, graph;
engine.drawObstructions();
graph = (function() {
var x, y, rows = [], cols, js = '[';
for(y = 0; y < 200; y += graphSettings.size) {
cols = [];
for(x = 0; x < 320; x += graphSettings.size) {
cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
}
js += '['+cols+'],\n';
rows.push(cols);
}
js = js.substring(0, js.length - 2);
js += ']';
document.getElementById('Graph').value=js;
return new Graph(rows, { diagonal: true });
})();
return engine;
}, start, end, engine = EngineBuilder(field, 10);
field.addEventListener('click', function(event) {
var position = engine.getPosition(event);
if(!start) {
start = position;
} else {
end = position;
}
if(start && end) {
engine.drawObstructions();
engine.drawPath(start, end);
start = end;
}
}, false);
#field {
border: thin black solid;
width: 98%;
background: #FFFFC7;
}
#Graph {
width: 98%;
height: 300px;
overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>

How do I reuse objects in an array for a particle system in JavaScript/jQuery?

I'm in the process of building an entity system for a canvas game. This started from a simple particle emitter/updater which I am altering to accommodate a multi-particle/entity generator. Whilst I am usually ok with JavaScript/jQuery I am running into the limits of my experience as it concerns arrays and would gratefully accept any help on the following:
When I need a new particle/entity my current system calls a function to push an object into an array which contains variables for the entity updates.
Then the update function runs a for loop over the array, checking on the type variable to update the particle (position/colour/etc...). Previously I would then [array.splice] the particle, based on some condition. When I needed further particles/entities I would then push new particles.
What I would like to achieve here is:
In the makeParticle function, check over the particle array for any "dead" particles and if any are available reuse them, or push a new particle if not I have created a particleAlive var as a flag for this purpose.
var particles = [];
var playing = false;
function mousePressed(event) {
playing = !playing;
}
if(playing) {
makeParticle(1, 200, 200, 10, "blueFlame");
makeParticle(1, 300, 200, 10, "redFlame");
}
function makeParticle(numParticles, xPos, yPos, pRadius, pType) {
var i;
for (i = 0; i < numParticles; i++) {
var p = {
type : pType,
x : xPos,
y : yPos,
xVel : random(-0.5, 0.5),
yVel : random(-1, -3),
particleAlive : true,
particleRender : true,
size : pRadius
}; // close var P
particles.push(p);
// instead of pushing fresh particles all the time I would like the function, here, to check for free objects in the array
} // close for loop
} // close function makeParticle
function runtime() {
for(var i=0; i<particles.length; i++) {
var p = particles[i];
var thisType = p.type;
switch (thisType) {
case "blueFlame":
c.fillStyle = rgb(100,100,255);
c.fillCircle(p.x,p.y,p.size);
p.x += p.xVel;
p.y += p.yVel;
p.size*=0.9;
if (particles.size < 0.5) {
particleAlive = false;
particleRender = false;
} // close if
break;
case "redFlame":
c.fillStyle = rgb(255,100,100);
c.fillCircle(p.x,p.y,p.size);
p.x -= p.xVel;
p.y -= p.yVel;
p.size*=0.95;
if (particles.size < 0.5) {
particleAlive = false;
particleRender = false;
} // close if
break;
} // close switch
} // close function runtime
I've found previous answers to relate questions, but I've been unable to get it working within the makeParticle function, like how to assign the attributes of p to particle[j]:
var particleUseOldOrNew = function() {
for (var j = 0, len = particles.length; j < len; j++) {
if (particles[j].particleAlive === false)
// particles[j] = p;
return particle[j];
}
return null; // No dead particles found, create new "particles.push(p);" perhaps?
}
My personal opinion on the matter is that if you are making a new particle, it should be a new object, not a "re-using" of an old one with properties changed. Each new object should have a unique identifier, so if you need to track them (for development purposes, debugging, or later re-use), it is easy to do. Or at least keep a counter of the number of times you've re-used a particle object to represent a "new" particle! Though I guess if you've found that "re-using" improves performance (have you?), that's the way to go.
Anyway, enough pontificating, here is how I would do what you're asking (I assume speed is your main concern, so I did this with only native JS):
var particles = [];
//Function to create brand spanking new particle
function makeNewParticle(xPos, yPos, pRadius, pType){
return {
type : pType,
x : xPos,
y : yPos,
xVel : random(-0.5, 0.5),
yVel : random(-1, -3),
particleAlive : true,
particleRender : true,
size : pRadius
};
};
//Function to change the properties of an old particle to make a psuedo-new particle (seriously, why do you want to do this?)
function changeExistingParticle(existing, xPos, yPos, pRadius, pType){
existing.x = xPos;
existing.y = yPos;
existing.size = pRadius;
existing.type = pType;
return existing;
};
//Figure out the keys of dead particles in the particles[] array
function getDeadParticleKeys() {
var keys = [];
for(var p = 0; P < particles.length; p++) {
if (!particles[p].particleAlive) {
keys.push(p);
}
}
};
function makeParticle(numParticles, xPos, yPos, pRadius, pType) {
var d, i, deadParticles;
//Grab the "dead" particle keys
deadParticleKeys = getDeadParticleKeys();
numParticles -= deadParticleKeys.length;
//Replace each dead particle with a "live" one at a specified key
for (d = 0; d < deadParticleKeys.length; d++) {
particles[ deadParticleKeys[d] ] = changeExistingParticle(particles[ deadParticleKeys[d] ], xPos, yPos, pRadius, pType)
}
//If we had more particles than there were dead spaces available, add to the array
for (i = 0; i < numParticles; i++) {
particles.push( makeNewParticle(xPos, yPos, pRadius, pType) );
}
};
Now, here's how I recommend doing it: abandon the idea or "re-using" particles, make a separate constructor for each particle (will help immensely if you add methods to your particles in the future), and just scrap dead particles every time one is added:
//Make a constructor for a particle
var Particle = function(props){
if (typeof props === 'function') {
props = props();
}
this.type = props.type;
this.x = props.x;
this.y = props.y;
this.size = props.size;
};
Paticle.prototype.particleAlive = true;
Paticle.prototype.particleRender = true;
//Global particles list
var particles = [];
//Remove all dead element from a ParticleList
particles.clean = function(){
var p, keys;
for (p = this.length; p >= 0; p--) {
if (!p.particleAlive) {
this.splice(p, 1);
}
}
};
//Method for adding x amount of new particles - if num parameter isn't provided, just assume it to be 1
particles.add = function(props, num){
//First, clean out all the garbage!
this.clean();
//Now, append new particles to the end
var n, limit = (num && typeof num === 'number') ? num : 1;
for (n = 0; n < limit; n++){
particles.push( new Particle(props) );
}
};
//A couple examples
particles.add({ //Add a single blueFlame
type: "blueFlame",
size: 10,
x: 200,
y: 200
});
particles.add({ //Add 4 redFlames
type: "redFlame",
size: 10,
x: 300,
y: 200
}, 4);
particles.add(function(){//Add 4 greenFlames, with randomized XY cooridinates
this.x = Math.round(Math.random() * 1000);
this.y = Math.round(Math.random() * 1000);
this.size = 20;
this.type = "greenFlame";
}, 4);
Way less code to manage. I'm not sure which way is faster, but I'd bet the speed difference is negligible. Of course, you could check for yourself by making a quick jsPerf.

HTML5 Canvas game development complication

I'm building a game using HTML5 canvas.
You can find it here, along with the source code: www.techgoldmine.com.
I'd make a jsFiddle, but in all honesty my attention span is too short (and myself mostly too stupid) to learn how it works.
I'm currently stuck at a function that looks at the positioning of certain elements on either side of the canvas and moves them so that the y-axis area they cover does not overlap. I call them turbines, but thin white rectangles would be more accurate. I suggest refreshing a few times to visually understand what's going on.
This is the function that spawns the turbines:
function gameStateNewLevel(){
for (var i = 0; i < 4; i++){
turbine = {};
turbine.width = 10;
turbine.height = 150;
turbine.y = Math.floor(Math.random()*600)
if (Math.random()*10 > 5){
turbine.side = leftSide;
}else{
turbine.side = rightSide;
}
turbine.render = function (){
context.fillStyle = "#FFFFFF"
context.fillRect(turbine.side, turbine.y, turbine.width,turbine.height);
}
turbine.PositionTop = turbine.y;
turbine.PositionBottom = turbine.y + turbine.height;
turbines.push(turbine);
}
context.fillStyle = "#FFFFFF"
switchGameState(GAME_STATE_PLAYER_START);
}
So far I've built (with the help of you wonderful people) a function (that is part of a loop) picking out each of these turbines, and starts comparing them to one another. I'm completely stumped when it comes to understanding how I'll get them to move and stop when needed:
function updateTurbines(){
var l = turbines.length-1;
for (var i = 0; i < l; i++){
var tempTurbine1 = turbines[i];
tempTurbine1.PositionTop = tempTurbine1.y;
tempTurbine1.PositionBottom = tempTurbine1.y + tempTurbine1.height;
for (var j = 0; j < l; j++) {
var tempTurbine2 = turbines[j];
tempTurbine2.PositionTop = tempTurbine2.y;
tempTurbine2.PositionBottom = tempTurbine2.y + tempTurbine2.height;
if ((tempTurbine1 !== tempTurbine2) && FIXME == true){
if(tempTurbine1.PositionBottom >= tempTurbine2.PositionTop){
turbines[j].y -=2;
//A while loop breaks the browser :(
}
}
}FIXME = false;
}
}
Any ideas or requests for additional explanation and info are more than welcome. I also have a feeling I'm severely over complicating this. Goddamn my head hurts. Bless you.
I'm afraid your code is a little bit messy do I decided to begin with a clean slate.
Use getters/setters for bottom and right. You can calculate them given the left/width and top/height values, respectively. This will save you from altering the complementary variable right when modifying e.g. left.
You seem to be looking for a collison detection algorithm for rectangles. This is quite easy if the rectangles have the same x-coordinate - two such rectangles do not collide if the bottom of the first is above the top of the other, or if the top of the first is under the bottom of the other. Use this algorithm along with a while loop to generate a new turbine as long as they collide.
This is what I ended up with (it's a separate piece of code as I stated, so you'll have to blend it into your game): http://jsfiddle.net/eGjak/249/.
var ctx = $('#cv').get(0).getContext('2d');
var Turbine = function(left, top, width, height) {
this.left = left;
this.top = top;
this.width = width;
this.height = height;
};
Object.defineProperties(Turbine.prototype, {
bottom: {
get: function() {
return this.top + this.height;
},
set: function(v) {
this.top = v - this.height;
}
},
right: {
get: function() {
return this.left + this.width;
},
set: function(v) {
this.left = v - this.width;
}
}
});
var turbines = [];
function turbineCollides(tn) {
for(var i = 0; i < turbines.length; i++) {
var to = turbines[i];
// they do not collide if if one's completely under
// the other or completely above the other
if(!(tn.bottom <= to.top || tn.top >= to.bottom)) {
return true;
}
}
return false; // this will only be executed if the `return true` part
// was never executed - so they the turbine does not collide
}
function addTurbine() {
var t, h;
do { // do while loop because the turbine has to be generated at least once
h = Math.floor(Math.random() * 300);
t = new Turbine(0, h, 15, 100);
} while(turbineCollides(t));
turbines.push(t);
}
// add two of them (do not add more than 4 because they will always collide
// due to the available space! In fact, there may be no space left even when
// you've added 2.)
addTurbine();
addTurbine();
function draw() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 400, 400);
ctx.fillStyle = "white";
for(var i = 0; i < turbines.length; i++) {
var turbine = turbines[i];
ctx.fillRect(turbine.left, turbine.top,
turbine.width, turbine.height);
}
}
draw();ā€‹

Categories

Resources