I've got an image like this:
The little circles are not regular but they are all black.
I'm looking for an algorithm to obtain an array made of all the rectangle bounds of every shape, like this:
I'm trying to do this with HTML5 canvas, but I'm not finding a way to get the bounds and go ahead with next shape. What I've done is obtaining the 2d array with x and y position of black pixels:
var x = 0,
y = 0,
cells = [];
for (var i = 0; i < imgdata.data.length; i += 4) {
if (imgdata.data[i] != 255) {
var p = [x,y];
cells.push(p);
}
if (x > image.width) {
x = 0;
y = y+1;
}
x = x+1;
}
console.log(cells);
You can do this naively with a simple flood fill-style algorithm that keeps track of the max/min points of each black area in all 4 directions.
The algorithm may visit every pixel twice and uses linear space. It also uses recursion, so it'll blow the stack if your black blobs are too large. Refactoring to use an explicit stack would avoid that. In other words, it's not optimized at all, just a proof of concept.
Here's the basic algorithm without a canvas. Note that it gives the points only within grid bounds, which would technically put the box on the edges of the shape, but you can add/subtract 1 (or another padding amount) to each corner to move the bounding box outside of the shape as desired.
const findBoundingBoxes = grid => {
const flood = (i, j, best) => {
if (i < 0 || j < 0 ||
i >= grid.length || j >= grid[i].length ||
!grid[i][j] || visited[i][j]) {
return;
}
visited[i][j] = true;
best.top = Math.min(best.top, i);
best.bottom = Math.max(best.bottom, i);
best.left = Math.min(best.left, j);
best.right = Math.max(best.right, j);
for (let di = -1; di < 2; di++) {
for (let dj = -1; dj < 2; dj++) {
if (di !== 0 || dj !== 0) {
flood(i + di, j + dj, best);
}
}
}
};
const boxes = [];
const visited = [...Array(grid.length)]
.map((_, i) => [...Array(grid[i].length)].fill(false));
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
if (!grid[i][j] || visited[i][j]) {
continue;
}
const best = {top: i, bottom: i, left: j, right: j};
flood(i, j, best);
boxes.push({
topLeft: {x: best.left, y: best.top},
topRight: {x: best.right, y: best.top},
bottomLeft: {x: best.left, y: best.bottom},
bottomRight: {x: best.right, y: best.bottom},
});
}
}
return boxes;
};
const grid = [
"0000000000000",
"0001100001000",
"0011110111110",
"0001100111100",
"0000000000000",
].map(e => e.split("").map(Number));
console.log(findBoundingBoxes(grid));
Now, on a canvas. I'm converting the image data to a 2d grid for ease of coding, which isn't performance-conscious.
const findBoundingBoxes = grid => {
const flood = (i, j, best) => {
if (i < 0 || j < 0 ||
i >= grid.length || j >= grid[i].length ||
!grid[i][j] || visited[i][j]) {
return;
}
visited[i][j] = true;
best.top = Math.min(best.top, i);
best.bottom = Math.max(best.bottom, i);
best.left = Math.min(best.left, j);
best.right = Math.max(best.right, j);
for (let di = -1; di < 2; di++) {
for (let dj = -1; dj < 2; dj++) {
if (di !== 0 || dj !== 0) {
flood(i + di, j + dj, best);
}
}
}
};
const boxes = [];
const visited = [...Array(grid.length)]
.map((_, i) => [...Array(grid[i].length)].fill(false));
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
if (!grid[i][j] || visited[i][j]) {
continue;
}
const best = {top: i, bottom: i, left: j, right: j};
flood(i, j, best);
boxes.push({
topLeft: {x: best.left, y: best.top},
topRight: {x: best.right, y: best.top},
bottomRight: {x: best.right, y: best.bottom},
bottomLeft: {x: best.left, y: best.bottom},
});
}
}
return boxes;
};
const canvas = document.createElement("canvas");
canvas.style.border = "1px solid blue";
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = function() {
const {width: w, height: h} = this;
canvas.width = w;
canvas.height = h;
ctx.drawImage(this, 0, 0, w, h);
const {data} = ctx.getImageData(0, 0, w, h);
const grid = [...Array(h)].map((_, i) =>
[...Array(w)].map((_, j) => data[(i*w+j)*4] < 255 ? 1 : 0)
);
const pad = 1;
ctx.strokeStyle = "red";
for (const box of findBoundingBoxes(grid)) {
const w = box.topRight.x - box.topLeft.x;
const h = box.bottomLeft.y - box.topLeft.y;
ctx.strokeRect(
box.topLeft.x - pad + 0.5,
box.topLeft.y - pad + 0.5,
w + pad * 2, h + pad * 2
);
}
};
img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TxSIVB4uIimSoThZERRylikWwUNoKrTqYXPoFTRqSFBdHwbXg4Mdi1cHFWVcHV0EQ/ABxdHJSdJES/5cUWsR4cNyPd/ced+8AoV5mqtkxAaiaZSRjUTGTXRW7XuFHAP0YwZDETD2eWkzDc3zdw8fXuwjP8j735+hRciYDfCLxHNMNi3iDeGbT0jnvE4dYUVKIz4nHDbog8SPXZZffOBccFnhmyEgn54lDxGKhjeU2ZkVDJZ4mDiuqRvlCxmWF8xZntVxlzXvyFwZz2kqK6zSHEcMS4khAhIwqSijDQoRWjRQTSdqPevgHHX+CXDK5SmDkWEAFKiTHD/4Hv7s181OTblIwCnS+2PbHKNC1CzRqtv19bNuNE8D/DFxpLX+lDsx+kl5raeEjoHcbuLhuafIecLkDDDzpkiE5kp+mkM8D72f0TVmg7xboXnN7a+7j9AFIU1fLN8DBITBWoOx1j3cH2nv790yzvx9wDHKmj+oRMgAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+YDCBYgCuMnEn4AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAC7klEQVR42u3d0WrkMAxA0Trk/3/ZfS20U5jBUizr3NeFZRmfkZ1smo4555e0ustHILAElsCSwBJYAksCS2AJLAksgSWwJLAElsCSwBJYAksCS2AJLAks7di92z9ojPHqj/wIZKHGDqv1DybIwEoihRdYgaTwAiuQFF6uCmNVhf7N2h1W9Nqz1RFWzqqz1QtW5nqz1QVW/kqzdT6sp9aYrZNhPbu6bHW5KhRYhwwMQ8vEElilRoWhZWIJrFJDwtAysQSWBJbKw9rzQOOYZWIJLAksgSWwJLAElsCSEmDt+RPJfk7axBJYElgqD2u3A40DloklsIoMCePKxBJYRUaFcWViCawiA8O4OnliPbW6VJ2/FeavMVVdzliZK01Vr8N7znpT1fGqMHrVqWoKK3Ttqdoqv0tHZ8FawgspsBbzQgqsZchgAkuuCiWwBJbAksBSiW4fQeFL+jfv/2XeAXC7oQWpfGdgdSSVYMsZC1OwVIisrdCwiTBgYslWKLAElgSWwBJYqteqewRB95vAkomllGEDlvalCZbAUp2dFKymOKLPZx5NPh/cz+cg0s77JpYxZmJpV0a/86CfTKyPvz3vPGzpmwbWYlLqBeuzN2YhBdYCc6+urgWWLe+o3McSWAJLYEltDu8O4yaWVGdiPZL/zDGxqAJLYElgCSwHrH4VeII09J4WUn0n1pzT8oNltMgZS53PWEFHLiPQxFoPgioTa/0Mowos2QolsASWwJLAElgCSwJLYAksCSyBJbAElo9AYKlMXgryR55+XvAZetBvuSfCwMp+w1ufT/twWBu+GbCJrXNgFXq7ZAdbN1ICCyZbIVK9d8OLKnWHRRVYVHX/ktzWQM5YAsu4AksCS2AJLAksgSWwJLAElsCSwBJYAksCS2AJLAksgSWwpMNheX0XWGzJViiwBJbdUNUmFltgsSVnLIFlaIElsKplaIElsGTugmU3BIstsCSwDC2w1PErcVkhgcUWWALL0BJYbNXoG/O8PCbkm47lAAAAAElFTkSuQmCC";
Related
I've been trying to procedurally generate a planet for the game I'm making in Javascript. It generated a random noise grid and then iterates through it using cellular automata rules. This takes longer than 30 seconds for worlds larger than 1000x1000, so I was wondering if there was a way to generate it from a seed when it is loaded, like in Minecraft. Is there a way to generate the planet without having to do it all at once, or at least a faster way?
Here's the code, it generates the terrain and then displays it on the canvas.
scale = 0.25;
rngCount = 0;
function* RNG() {
let seed = Math.floor(Math.random() * 10000) / 10000;
console.log(seed);
let factor = 297;
while(true) {
seed = (seed * factor) - Math.floor(seed * factor);
yield Math.floor(seed * 10000) / 10000;
};
};
let generationRNG = RNG();
function clamp(min, max, value) {
if(value < min) {
return min;
} else if(value > max) {
return max;
} else {
return value;
};
};
let map = [];
let mapW = 4096;
let mapH = 4096;
function iterate(e, map) {
let height = map.length;
let width = map[0].length;
let updatedMap;
let mapRow;
let neighborCount;
let iy;
let ix;
for(i = 0; i < e; i++) {
updatedMap = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
neighborCount = 0;
for(k = -1; k <= 1; k++) {
for(l = -1 ; l <= 1; l++) {
if(y + k < 0) {
iy = height - 1;
} else if(y + k >= height) {
iy = 0;
} else {
iy = y + k;
};
if(x + l < 0) {
ix = width - 1;
} else if(x + l >= width) {
ix = 0;
} else {
ix = x + l;
};
if(!(l == 0 && k == 0)) {
neighborCount += map[iy][ix];
};
};
};
if(neighborCount >= 5) {
mapRow.push(1);
} else if(neighborCount <= 3) {
mapRow.push(0);
} else {
mapRow.push(map[y][x]);
};
};
updatedMap.push(mapRow);
};
map = updatedMap;
};
return map;
};
function terrainGeneration() {
let width = mapW / 16;
let height = mapH / 16;
let mapRow;
let factor = 4;
let fillPercent = 0.45
let updatedMap;
let map = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
if(generationRNG.next().value <= fillPercent) {
mapRow.push(1);
} else {
mapRow.push(0);
};
};
map.push(mapRow);
};
map = iterate(10, map);
for(a = 0; a < 2; a++) {
if(a == 0) {
fillPercent = 0.33;
} else if (a == 1) {
fillPercent = 0.2;
};
width *= factor;
height *= factor;
updatedMap = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
mapRow.push(map[Math.floor(y / factor)][Math.floor(x / factor)]);
};
updatedMap.push(mapRow);
};
map = updatedMap;
updatedMap = [];
for(y = 0; y < height; y++) {
mapRow = [];
for(x = 0; x < width; x++) {
if(map[y][x] == 1) {
if(generationRNG.next().value <= fillPercent) {
mapRow.push(0);
} else {
mapRow.push(1);
};
} else {
if(generationRNG.next().value <= fillPercent) {
mapRow.push(1);
} else {
mapRow.push(0);
};
};
};
updatedMap.push(mapRow);
};
map = updatedMap;
map = iterate(5, map);
};
return map;
};
map = terrainGeneration();
for(i = 0; i < map.length; i++) {
for(j = 0; j < map[i].length; j++) {
c.fillStyle = "rgba(" + map[i][j] * 255 + ", " + map[i][j] * 255 + ", " + map[i][j] * 255 + ", 1)";
c.fillRect(j * scale, i * scale, scale, scale);
};
};
c.fillStyle = "rgba(0, 0, 255, 1)";
c.fillRect(0, 0, 40 * scale, 40 * scale);
is there a way to generate only a section of it from a seed and still have it coherently fit together? After I add biomes, the generation will take even longer, so I want to try to fix it now before I add more code. To test out the section generation I was going to have it generate the area around the cursor only.
here's an example of a 4096x4096 map that took 3 and a half minutes to generate.
jsfiddle: https://jsfiddle.net/nragrkb8/3/
I'm trying to get a spectrogram drawn, works in chrome but draws nothing in all other browsers. I'm using getImageData and putImageData to move existing contents down by a pixel every time new data is pushed in. That new data is then drawn into an offscreen image and then putimagedata to apply it to the full canvas.
Spectrogram = (function() {
//constructor function
var Spectrogram = function(options) {
//compose options
this.canvas = options.canvas;
this.width = options.size; //the size of the FFT
this.height = options.length; //the number of FFT to keep
if (typeof options.min !== 'undefined' && typeof options.max !== 'undefined') {
this.rangeType = 'static';
this.min = options.min;
this.max = options.max;
} else {
this.rangeType = 'auto';
this.min = Infinity;
this.max = -Infinity;
}
//set the function used to determine a value's color
this.colorScaleFn = options.colorScaleFn || defaultColorScaling;
//precalculate the range used in color scaling
this.range = this.max - this.min;
//the 2d drawing context
this.ctx = this.canvas.getContext('2d', { alpha: false });
//single pixel image used to draw new data points
this.im = this.ctx.createImageData(this.width, 1);
this.canvas.width = this.width;
this.canvas.height = this.height;
//adds a new row of data to the edge of the spectrogram, shifting existing
//contents by 1px
this.addData = function (newData) {
if (newData.length != this.width) {
console.error('Unmatched dataset size. Expected ' + this.width + ', was + ' + newData.length);
return;
}
if (this.rangeType == 'auto') {
var changed = false;
for (var i = 0; i < newData.length; ++i) {
if (newData[i] < this.min) {
this.min = newData[i];
changed = true;
} else if (newData[i] > this.max) {
this.max = newData[i];
changed = true;
}
}
if (changed) {
this.range = this.max - this.min;
}
}
//move the current contents by 1px
var im = this.ctx.getImageData(0, 0, this.width, this.height - 1);
this.ctx.putImageData(im, 0, 1);
//draw the new data values into the temporary image
var j = 0;
for (var i = 0; i < newData.length; ++i) {
var c = this.colorScaleFn(newData[i]);
this.im.data[j] = c[0];
this.im.data[j + 1] = c[1];
this.im.data[j + 2] = c[2];
j += 4;
}
//put the new data colors into the full canvas
this.ctx.putImageData(this.im, 0, 0);
}
}
function defaultColorScaling(value) {
var x = (value - this.min) / this.range;
if (x < 0) {
x = 0;
} else if (x > 1) {
x = 1;
}
var g = Math.pow(x, 2);
var b = 0.75 - 1.5 * Math.abs(x - 0.45);
var r = 0;
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
return Spectrogram;
})();
const size = 1024;
const length = 200;
const dataMax = 100;
const velMax = 1;
const canvas = document.getElementById('spec');
canvas.width = size;
canvas.height = length;
const spec = new Spectrogram({
canvas: canvas,
size: size,
length: length
});
const data = [];
const vel = [];
for (var i = 0; i < size; ++i) {
data.push(0);
vel.push(0);
}
setInterval(function() {
for (var i = 0; i < size; ++i) {
data[i] += vel[i];
if (i > 0) data[i] += vel[i - 1];
if (i < size - 1) data[i] += vel[i + 1];
if (data[i] >= dataMax) {
data[i] = dataMax;
vel[i] = 0;
} else if (data[i] <= -dataMax) {
data[i] = -dataMax;
vel[i] = 0;
}
if (vel[i] == 0) {
vel[i] = (Math.random() - 0.5) * 100;
}
}
spec.addData(data);
}, 100);
Your problem was induced by a misconception:
getContext('2d', {alpha: false}) should have no incidence on createImageData function, so you still have to set the alpha value of your ImageData to something else than 0.
There are also other things in your code that don't go well, even if unrealted with your current issue:
You should avoid at all costs an high frequency setInterval where you perform a task that can take more than the interval (and in your case it can), but rather use something like a requestAnimationFrame loop.
Don't use getImageData + putImageData to draw your context on itself, simply use ctx.drawImage(ctx.canvas, x, y).
There might be other things to fix in your code, but here is an update with these three major things:
Spectrogram = (function() {
//constructor function
var Spectrogram = function(options) {
//compose options
this.canvas = options.canvas;
this.width = options.size; //the size of the FFT
this.height = options.length; //the number of FFT to keep
if (typeof options.min !== 'undefined' && typeof options.max !== 'undefined') {
this.rangeType = 'static';
this.min = options.min;
this.max = options.max;
} else {
this.rangeType = 'auto';
this.min = Infinity;
this.max = -Infinity;
}
//set the function used to determine a value's color
this.colorScaleFn = options.colorScaleFn || defaultColorScaling;
//precalculate the range used in color scaling
this.range = this.max - this.min;
//the 2d drawing context
this.ctx = this.canvas.getContext('2d', {
alpha: false
});
//single pixel image used to draw new data points
this.im = this.ctx.createImageData(this.width, 1);
this.canvas.width = this.width;
this.canvas.height = this.height;
//adds a new row of data to the edge of the spectrogram, shifting existing
//contents by 1px
this.addData = function(newData) {
if (newData.length != this.width) {
console.error('Unmatched dataset size. Expected ' + this.width + ', was + ' + newData.length);
return;
}
if (this.rangeType == 'auto') {
var changed = false;
for (var i = 0; i < newData.length; ++i) {
if (newData[i] < this.min) {
this.min = newData[i];
changed = true;
} else if (newData[i] > this.max) {
this.max = newData[i];
changed = true;
}
}
if (changed) {
this.range = this.max - this.min;
}
}
//move the current contents by 1px
this.ctx.drawImage(this.ctx.canvas, 0, 1)
//draw the new data values into the temporary image
var j = 0;
for (var i = 0; i < newData.length; ++i) {
var c = this.colorScaleFn(newData[i]);
this.im.data[j] = c[0];
this.im.data[j + 1] = c[1];
this.im.data[j + 2] = c[2];
// don't forget the alpha channel, createImageData is always full of zeroes
this.im.data[j + 3] = 255;
j += 4;
}
this.ctx.putImageData(this.im, 0, 0);
}
}
function defaultColorScaling(value) {
var x = (value - this.min) / this.range;
if (x < 0) {
x = 0;
} else if (x > 1) {
x = 1;
}
var g = Math.pow(x, 2);
var b = 0.75 - 1.5 * Math.abs(x - 0.45);
var r = 0;
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
return Spectrogram;
})();
const size = 1024;
const length = 200;
const dataMax = 100;
const velMax = 1;
const canvas = document.getElementById('spec');
canvas.width = size;
canvas.height = length;
const spec = new Spectrogram({
canvas: canvas,
size: size,
length: length
});
const data = [];
const vel = [];
for (var i = 0; i < size; ++i) {
data.push(0);
vel.push(0);
}
// our animation loop
function draw() {
for (var i = 0; i < size; ++i) {
data[i] += vel[i];
if (i > 0) data[i] += vel[i - 1];
if (i < size - 1) data[i] += vel[i + 1];
if (data[i] >= dataMax) {
data[i] = dataMax;
vel[i] = 0;
} else if (data[i] <= -dataMax) {
data[i] = -dataMax;
vel[i] = 0;
}
if (vel[i] == 0) {
vel[i] = (Math.random() - 0.5) * 100;
}
}
spec.addData(data);
requestAnimationFrame(draw);
}
draw();
<div style="width: 100%; height: 100%; padding: 0; margin: 0;">
<canvas style="width: 100%; height: 100%;" id="spec"></canvas>
</div>
I'm developing a tool for modifying different geometrical shapes from an assortment of templates. The shapes are basic ones that could be found in rooms.
For example: L-shape, T-shape, Hexagon, Rectangle etc.
What I need to do is making the shape conform all necessary edges as to keep the shape's symmetry and bounding dimensions intact when the user modifies an edge.
A shape is simply implemented like this, with the first node starting in the upper left corner and going around the shape clockwise (I use TypeScript):
public class Shape {
private nodes: Array<Node>;
private scale: number; // Scale for calculating correct coordinate compared to given length
... // A whole lot of transformation methods
Which then is drawn as a graph, connecting each node to the next in the array. (See below)
If I, for example, would change the length of edge C to 3m from 3.5m, then I'd also want either edge E or G to change their length to keep the side to 12m and also push down E so that edge D is still fully horizontal.
If I instead would change side D to 2m, then B would have to change its length to 10m and so on.
(I do have shapes which have slanted angles as well, like a rectangle with one of its corners cut off)
The problem
I have the following code for modifying the specific edge:
public updateEdgeLength(start: Point, length: number): void {
let startNode: Node;
let endNode: Node;
let nodesSize = this.nodes.length;
// Find start node, and then select end node of selected edge.
for (let i = 0; i < nodesSize; i++) {
if (this.nodes[i].getX() === start.x && this.nodes[i].getY() === start.y) {
startNode = this.nodes[i];
endNode = this.nodes[(i + 1) % nodesSize];
break;
}
}
// Calculate linear transformation scalar and create a vector of the edge
let scaledLength = (length * this.scale);
let edge: Vector = Vector.create([endNode.getX() - startNode.getX(), endNode.getY() - startNode.getY()]);
let scalar = scaledLength / startNode.getDistance(endNode);
edge = edge.multiply(scalar);
// Translate the new vector to its correct position
edge = edge.add([startNode.getX(), startNode.getY()]);
// Calculate tranlation vector
edge = edge.subtract([endNode.getX(), endNode.getY()]);
endNode.translate({x: edge.e(1), y: edge.e(2)});
}
Now I need a more general case for finding the corresponding edges that will also need to be modified. I have begun implementing shape-specific algorithms as I know which nodes correspond to the edges of the shape, but this won't be very extensible for the future.
For example, the shape above could be implemented somewhat like this:
public updateSideLength(edge: Position): void {
// Get start node coordinates
let startX = edge.start.getX();
let startY = edge.start.getY();
// Find index of start node;
let index: num;
for (let i = 0; i < this.nodes.length; i++) {
let node: Node = this.nodes[i];
if(node.getX() === startX && node.getY() === startY) {
index = i;
break;
}
}
// Determine side
let side: number;
if (index === 0 || index === 2) {
side = this.TOP;
}
else if (index === 1 || index === 3 || index === 5) {
side = this.RIGHT;
}
else if (index === 4 || index === 6) {
side = this.BOTTOM;
}
else if (index === 7) {
side = this.LEFT;
}
adaptSideToBoundingBox(index, side); // adapts all other edges of the side except for the one that has been modified
}
public adaptSideToBoundingBox(exceptionEdge: number, side: number) {
// Modify all other edges
// Example: C and G will be modified
Move C.end Y-coord to D.start Y-coord;
Move G.start Y-coord to D.end Y-coord;
}
And so on.. But implementing this for each shape (5 atm.) and for future shapes would be very time consuming.
So what I'm wondering is if there is a more general approach to this problem?
Thanks!
Keep a list of point pairs and the key that constrains them and use that to overwrite coordinates on update.
This works with the example you gave:
var Point = (function () {
function Point(x, y, connectedTo) {
if (connectedTo === void 0) { connectedTo = []; }
this.x = x;
this.y = y;
this.connectedTo = connectedTo;
}
return Point;
}());
var Polygon = (function () {
function Polygon(points, constrains) {
if (constrains === void 0) { constrains = []; }
this.points = points;
this.constrains = constrains;
}
return Polygon;
}());
var Sketch = (function () {
function Sketch(polygons, canvas) {
if (polygons === void 0) { polygons = []; }
if (canvas === void 0) { canvas = document.body.appendChild(document.createElement("canvas")); }
this.polygons = polygons;
this.canvas = canvas;
this.canvas.width = 1000;
this.canvas.height = 1000;
this.ctx = this.canvas.getContext("2d");
this.ctx.fillStyle = "#0971CE";
this.ctx.strokeStyle = "white";
this.canvas.onmousedown = this.clickHandler.bind(this);
this.canvas.onmouseup = this.clickHandler.bind(this);
this.canvas.onmousemove = this.clickHandler.bind(this);
requestAnimationFrame(this.draw.bind(this));
}
Sketch.prototype.clickHandler = function (evt) {
if (evt.type == "mousedown") {
if (this.selectedPoint != void 0) {
this.selectedPoint = null;
}
else {
var score = null;
var best = null;
for (var p = 0; p < this.polygons.length; p++) {
var polygon = this.polygons[p];
for (var pi = 0; pi < polygon.points.length; pi++) {
var point = polygon.points[pi];
var dist = Math.abs(point.x - evt.offsetX) + Math.abs(point.y - evt.offsetY);
if (score == null ? true : dist < score) {
score = dist;
best = point;
}
}
}
this.selectedPoint = best;
}
}
if (evt.type == "mousemove" && this.selectedPoint != void 0) {
this.selectedPoint.x = Math.round(evt.offsetX / 5) * 5;
this.selectedPoint.y = Math.round(evt.offsetY / 5) * 5;
for (var pi = 0; pi < this.polygons.length; pi++) {
var polygon = this.polygons[pi];
if (polygon.points.indexOf(this.selectedPoint) < 0) {
continue;
}
for (var pa = 0; pa < polygon.constrains.length; pa++) {
var constrain = polygon.constrains[pa];
if (constrain.a == this.selectedPoint || constrain.b == this.selectedPoint) {
constrain.a[constrain.key] = this.selectedPoint[constrain.key];
constrain.b[constrain.key] = this.selectedPoint[constrain.key];
if (constrain.offset != void 0) {
if (constrain.a == this.selectedPoint) {
constrain.b[constrain.key] += constrain.offset;
}
else {
constrain.a[constrain.key] -= constrain.offset;
}
}
}
}
}
}
requestAnimationFrame(this.draw.bind(this));
};
Sketch.prototype.draw = function () {
var ctx = this.ctx;
//clear
ctx.fillStyle = "#0971CE";
ctx.fillRect(0, 0, 1000, 1000);
//grid
ctx.strokeStyle = "rgba(255,255,255,0.25)";
for (var x = 0; x <= this.canvas.width; x += 5) {
ctx.beginPath();
ctx.moveTo(x, -1);
ctx.lineTo(x, this.canvas.height);
ctx.stroke();
ctx.closePath();
}
for (var y = 0; y <= this.canvas.height; y += 5) {
ctx.beginPath();
ctx.moveTo(-1, y);
ctx.lineTo(this.canvas.width, y);
ctx.stroke();
ctx.closePath();
}
ctx.strokeStyle = "white";
ctx.fillStyle = "white";
//shapes
for (var i = 0; i < this.polygons.length; i++) {
var polygon = this.polygons[i];
for (var pa = 0; pa < polygon.points.length; pa++) {
var pointa = polygon.points[pa];
if (pointa == this.selectedPoint) {
ctx.beginPath();
ctx.fillRect(pointa.x - 2, pointa.y - 2, 4, 4);
ctx.closePath();
}
ctx.beginPath();
for (var pb = 0; pb < pointa.connectedTo.length; pb++) {
var pointb = pointa.connectedTo[pb];
if (polygon.points.indexOf(pointb) < pa) {
continue;
}
ctx.moveTo(pointa.x, pointa.y);
ctx.lineTo(pointb.x, pointb.y);
}
ctx.stroke();
ctx.closePath();
}
}
};
return Sketch;
}());
//==Test==
//Build polygon 1 (House)
var poly1 = new Polygon([
new Point(10, 10),
new Point(80, 10),
new Point(80, 45),
new Point(130, 45),
new Point(130, 95),
new Point(80, 95),
new Point(80, 135),
new Point(10, 135),
]);
//Connect dots
for (var x = 0; x < poly1.points.length; x++) {
var a = poly1.points[x];
var b = poly1.points[(x + 1) % poly1.points.length];
a.connectedTo.push(b);
b.connectedTo.push(a);
}
//Setup constrains
for (var x = 0; x < poly1.points.length; x++) {
var a = poly1.points[x];
var b = poly1.points[(x + 1) % poly1.points.length];
poly1.constrains.push({ a: a, b: b, key: x % 2 == 1 ? 'x' : 'y' });
}
poly1.constrains.push({ a: poly1.points[1], b: poly1.points[5], key: 'x' }, { a: poly1.points[2], b: poly1.points[5], key: 'x' }, { a: poly1.points[1], b: poly1.points[6], key: 'x' }, { a: poly1.points[2], b: poly1.points[6], key: 'x' });
//Build polygon 2 (Triangle)
var poly2 = new Polygon([
new Point(250, 250),
new Point(300, 300),
new Point(200, 300),
]);
//Connect dots
for (var x = 0; x < poly2.points.length; x++) {
var a = poly2.points[x];
var b = poly2.points[(x + 1) % poly2.points.length];
a.connectedTo.push(b);
b.connectedTo.push(a);
}
//Setup constrains
poly2.constrains.push({ a: poly2.points[0], b: poly2.points[1], key: 'x', offset: 50 }, { a: poly2.points[0], b: poly2.points[1], key: 'y', offset: 50 });
//Generate sketch
var s = new Sketch([poly1, poly2]);
<!-- TYPESCRIPT -->
<!--
class Point {
constructor(public x: number, public y: number, public connectedTo: Point[] = []) {
}
}
interface IConstrain {
a: Point,
b: Point,
key: string,
offset?: number
}
class Polygon {
constructor(public points: Point[], public constrains: IConstrain[] = []) {
}
}
class Sketch {
public ctx: CanvasRenderingContext2D;
constructor(public polygons: Polygon[] = [], public canvas = document.body.appendChild(document.createElement("canvas"))) {
this.canvas.width = 1000;
this.canvas.height = 1000;
this.ctx = this.canvas.getContext("2d");
this.ctx.fillStyle = "#0971CE";
this.ctx.strokeStyle = "white";
this.canvas.onmousedown = this.clickHandler.bind(this)
this.canvas.onmouseup = this.clickHandler.bind(this)
this.canvas.onmousemove = this.clickHandler.bind(this)
requestAnimationFrame(this.draw.bind(this))
}
public selectedPoint: Point
public clickHandler(evt: MouseEvent) {
if (evt.type == "mousedown") {
if (this.selectedPoint != void 0) {
this.selectedPoint = null;
} else {
let score = null;
let best = null;
for (let p = 0; p < this.polygons.length; p++) {
let polygon = this.polygons[p];
for (let pi = 0; pi < polygon.points.length; pi++) {
let point = polygon.points[pi];
let dist = Math.abs(point.x - evt.offsetX) + Math.abs(point.y - evt.offsetY)
if (score == null ? true : dist < score) {
score = dist;
best = point;
}
}
}
this.selectedPoint = best;
}
}
if (evt.type == "mousemove" && this.selectedPoint != void 0) {
this.selectedPoint.x = Math.round(evt.offsetX / 5) * 5;
this.selectedPoint.y = Math.round(evt.offsetY / 5) * 5;
for (let pi = 0; pi < this.polygons.length; pi++) {
let polygon = this.polygons[pi];
if (polygon.points.indexOf(this.selectedPoint) < 0) {
continue;
}
for (let pa = 0; pa < polygon.constrains.length; pa++) {
let constrain = polygon.constrains[pa];
if (constrain.a == this.selectedPoint || constrain.b == this.selectedPoint) {
constrain.a[constrain.key] = this.selectedPoint[constrain.key]
constrain.b[constrain.key] = this.selectedPoint[constrain.key]
if (constrain.offset != void 0) {
if (constrain.a == this.selectedPoint) {
constrain.b[constrain.key] += constrain.offset
} else {
constrain.a[constrain.key] -= constrain.offset
}
}
}
}
}
}
requestAnimationFrame(this.draw.bind(this))
}
public draw() {
var ctx = this.ctx;
//clear
ctx.fillStyle = "#0971CE";
ctx.fillRect(0, 0, 1000, 1000)
//grid
ctx.strokeStyle = "rgba(255,255,255,0.25)"
for (let x = 0; x <= this.canvas.width; x += 5) {
ctx.beginPath()
ctx.moveTo(x, -1)
ctx.lineTo(x, this.canvas.height)
ctx.stroke();
ctx.closePath()
}
for (let y = 0; y <= this.canvas.height; y += 5) {
ctx.beginPath()
ctx.moveTo(-1, y)
ctx.lineTo(this.canvas.width, y)
ctx.stroke();
ctx.closePath()
}
ctx.strokeStyle = "white"
ctx.fillStyle = "white";
//shapes
for (let i = 0; i < this.polygons.length; i++) {
let polygon = this.polygons[i];
for (let pa = 0; pa < polygon.points.length; pa++) {
let pointa = polygon.points[pa];
if (pointa == this.selectedPoint) {
ctx.beginPath();
ctx.fillRect(pointa.x - 2, pointa.y - 2, 4, 4)
ctx.closePath();
}
ctx.beginPath();
for (var pb = 0; pb < pointa.connectedTo.length; pb++) {
var pointb = pointa.connectedTo[pb];
if (polygon.points.indexOf(pointb) < pa) {
continue;
}
ctx.moveTo(pointa.x, pointa.y)
ctx.lineTo(pointb.x, pointb.y)
}
ctx.stroke();
ctx.closePath();
}
}
}
}
//==Test==
//Build polygon 1 (House)
var poly1 = new Polygon([
new Point(10, 10),
new Point(80, 10),
new Point(80, 45),
new Point(130, 45),
new Point(130, 95),
new Point(80, 95),
new Point(80, 135),
new Point(10, 135),
])
//Connect dots
for (let x = 0; x < poly1.points.length; x++) {
let a = poly1.points[x];
let b = poly1.points[(x + 1) % poly1.points.length]
a.connectedTo.push(b)
b.connectedTo.push(a)
}
//Setup constrains
for (let x = 0; x < poly1.points.length; x++) {
let a = poly1.points[x];
let b = poly1.points[(x + 1) % poly1.points.length]
poly1.constrains.push({ a: a, b: b, key: x % 2 == 1 ? 'x' : 'y' })
}
poly1.constrains.push(
{ a: poly1.points[1], b: poly1.points[5], key: 'x' },
{ a: poly1.points[2], b: poly1.points[5], key: 'x' },
{ a: poly1.points[1], b: poly1.points[6], key: 'x' },
{ a: poly1.points[2], b: poly1.points[6], key: 'x' }
)
//Build polygon 2 (Triangle)
var poly2 = new Polygon([
new Point(250, 250),
new Point(300, 300),
new Point(200, 300),
])
//Connect dots
for (let x = 0; x < poly2.points.length; x++) {
let a = poly2.points[x];
let b = poly2.points[(x + 1) % poly2.points.length]
a.connectedTo.push(b)
b.connectedTo.push(a)
}
//Setup constrains
poly2.constrains.push(
{ a: poly2.points[0], b: poly2.points[1], key: 'x', offset: 50 },
{ a: poly2.points[0], b: poly2.points[1], key: 'y', offset: 50 },
)
//Generate sketch
var s = new Sketch([poly1, poly2])
-->
UPDATE - Constrain offsets
Based on feedback in the comments i added a "offset" key in the constrains to handle uneven relationships.
The Triangles top-right-most edge (at least initially) is constrained with an offset.
At my school I am learning how to code in JS using a site called codehs.com. After a while I learned about graphics with JS. There was this one point where I had to create a circle:
var circle = new Circle(50);
circle.setPosition(100,100);
add(circle);
After a few days I came across another website that was teaching students how code using JS. The website was called khanacademy.org I was interested and saw that the first lesson was making drawings. I looked at the video provided and it had a different code to make a circle.
ellipse(203, 197, 300, 350);
I am confused on how to make a circle using JS since I just started.
I'm one of the founders of CodeHS. CodeHS uses a custom JavaScript library on top of regular JavaScript. Khan Academy uses Processing JS, which is a different library (You can use Processing on CodeHS as well if you like).
You can see the documentation for everything in the CodeHS JS library at https://codehs.com/docs and learn how to use it in the Intro CS in JavaScript course.
We have designed this library to be great for learning -- it gives you experience using Object Oriented Programming while making it simple to create and manipulate shapes for programs like a Helicopter Game.
Additionally, you can include the library on an HTML page that runs JavaScript by adding this script tag to your page.
<script type="text/javascript" src="https://static.codehs.com/gulp/3d065bc81d3b7edf21e928ce2c764374a03c5cd6/chs-js-lib/chs.js"></script>
Here's an example of a full HTML page that runs JavaScript and uses the CodeHS library on it to draw a circle.
<html>
<head>
<title>Circle Example</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript" src="https://static.codehs.com/gulp/3d065bc81d3b7edf21e928ce2c764374a03c5cd6/chs-js-lib/chs.js"></script>
<style>
canvas {
border: 1px solid black;
display: inline-block;
vertical-align: top;
}
pre {
border: 1px solid black;
display: inline-block;
width: 400px;
height: 500px;
background-color: #F5F5F5;
}
</style>
</head>
<body>
<h1>Circle Example</h1>
<canvas
width="400"
height="500"
class="codehs-editor-canvas"></canvas>
<script>
window.onload = function() {
var circle = new Circle(50);
circle.setPosition(100,100);
add(circle);
};
</script>
</body>
</html>
Looks like KHAN ACADEMY uses ProcessingJS to draw the circle
I was unable to check what is the library CodeHS uses to draw a circle, but has to be a different one. But the fact is that there are so many good libraries developed in javascript to make whatever you can imagine. They're generally different one from another but their goal is to make our life easier.
JavaScript library | Wikipedia
What's a JS library? | KHAN ACADEMY
I tried using the processing platform for CodeHs, just copying and pasting this code:
/**
* This program finds the shortest path through a series of obstacles using A* search,
* smooths the path, then uses PID control to drive a robot to the goal.
**/
// You can play around with these constants---
var NUM_BLOCKS = 20;
var OBSTACLE_PROBABILITY = 0.2;
var MOVE_NOISE = 0.1;
var STEERING_NOISE = 0.1;
var ROBOT_SPEED = 0.5;
var MAX_STEERING_ANGLE = Math.PI/4.0;
var tP = 2.0;
var tI = 0.0001;
var tD = 16.0;
var weightData = 0.1;
var weightSmooth = 0.1;
// Search types
var A_STAR = 0;
var GREEDY = 1;
var BREADTH_FIRST = 2;
var DEPTH_FIRST = 3;
var SEARCH_MODE = A_STAR;
// --------------------------------------------
var BLOCK_SIZE = width/NUM_BLOCKS;
var START = 2;
var GOAL_X = NUM_BLOCKS-1;
var GOAL_Y = NUM_BLOCKS-1;
var GOAL = 3;
var START_COLOR = color(23, 33, 176);
var GOAL_COLOR = color(199, 188, 68);
var FRONTIER_COLOR = color(105, 179, 105);
var EXPLORED_COLOR = color(117, 117, 117, 100);
var PATH_COLOR = color(166, 53, 53);
var SMOOTH_PATH_COLOR = color(53, 68, 166);
var PLAN = 0;
var SMOOTH = 1;
var CALIBRATE = 2;
var NAVIGATE = 3;
var DELTA = [[-1, 0], [1, 0], [0, -1], [0, 1]];
var frontier = [[0, 0, 0, 0]];
var explored = [];
var predecessors = [];
var path = [];
var smoothPath = [];
var mode = PLAN;
var index = 0;
var cte = 0;
var sigma = 0;
angleMode = "radians";
frameRate(60);
// Initialize world
var world = [];
for (var i = 0; i < NUM_BLOCKS; i++) {
var row = [];
for (var j = 0; j < NUM_BLOCKS; j++) {
var r = random();
if (r < OBSTACLE_PROBABILITY) {
row.push(1);
}
else {
row.push(0);
}
}
world.push(row);
}
world[0][0] = START;
world[GOAL_Y][GOAL_X] = GOAL;
for (var i = 0; i < NUM_BLOCKS; i++) {
var row = [];
for (var j = 0; j < NUM_BLOCKS; j++) {
row.push(false);
}
explored.push(row);
}
explored[0][0] = true;
for (var i = 0; i < NUM_BLOCKS; i++) {
var row = [];
for (var j = 0; j < NUM_BLOCKS; j++) {
row.push(null);
}
predecessors.push(row);
}
var Robot = function(x, y, size) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.orientation = 0;
this.size = size;
// Thanks to Sebastian Thrun for this code:
// https://www.udacity.com/course/viewer#!/c-cs373/l-48696626/e-48403941/m-48729137
// My code for this movement can be found at:
// https://www.khanacademy.org/computer-programming/bicycle-model/5496951953031168
this.move = function(d, theta) {
var generator = new Random(millis());
var moveDistance = (generator.nextGaussian() * MOVE_NOISE) + d;
var turnAngle = (generator.nextGaussian() * STEERING_NOISE) + theta;
turnAngle = min(turnAngle, MAX_STEERING_ANGLE);
turnAngle = max(turnAngle, -MAX_STEERING_ANGLE);
var turn = tan(turnAngle)*moveDistance/this.size;
// Approximately straight motion
if (abs(turn) < 0.001) {
this.x += moveDistance*cos(this.orientation);
this.y += moveDistance*sin(this.orientation);
this.orientation = (this.orientation+turn)%(2.0*Math.PI);
}
// Move using the bicyle model
else {
var radius = moveDistance/turn;
var cx = this.x-(sin(this.orientation)*radius);
var cy = this.y+(cos(this.orientation)*radius);
this.orientation = (this.orientation+turn)%(2.0*Math.PI);
this.x = cx + (sin(this.orientation)*radius);
this.y = cy - (cos(this.orientation)*radius);
}
};
this.draw = function() {
pushMatrix();
translate(this.x, this.y);
rotate(this.orientation);
fill(128, 27, 27);
stroke(255, 0, 0);
rect(-this.size/2, -(this.size*0.75*0.5), this.size, this.size*0.75);
popMatrix();
};
};
var robot;
var addToFrontier = function(node) {
// Insert the node into the frontier
// Order by lowest cost
var i = frontier.length-1;
if (SEARCH_MODE === A_STAR) {
while (i > 0 && node[2]+node[3] < frontier[i][2]+frontier[i][3]) {
i--;
}
}
else if (SEARCH_MODE === GREEDY) {
while (i > 0 && node[3] < frontier[i][3]) {
i--;
}
}
else if (SEARCH_MODE === BREADTH_FIRST) {
frontier.push(node);
}
else if (SEARCH_MODE === DEPTH_FIRST) {
frontier.splice(0, 0, node);
}
frontier.splice(i+1, 0, node);
};
var distance = function(x1, y1, x2, y2) {
return sqrt(pow(x1-x2, 2) + pow(y1-y2, 2));
};
var manhattanDistance = function(x1, y1, x2, y2) {
return abs(x1-x2) + abs(y1-y2);
};
var drawWorld = function() {
background(255, 255, 255);
for (var i = 0; i < world.length; i++) {
for (var j = 0; j < world[0].length; j++) {
if (world[i][j] === 1) {
stroke(0, 0, 0);
fill(0, 0, 0);
}
else if (world[i][j] === START) {
fill(START_COLOR);
stroke(START_COLOR);
}
else if (world[i][j] === GOAL) {
fill(GOAL_COLOR);
stroke(GOAL_COLOR);
}
else {
fill(255, 255, 255);
noStroke();
}
rect(j*BLOCK_SIZE, i*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
}
};
var simulate = function(steps) {
var error = 0;
var crosstrackError = 0;
var r = new Robot(0, 1, BLOCK_SIZE/2);
var sigma = 0;
var diff = 0;
for (var i = 0; i < steps*2; i++) {
// Compute cte
diff = r.y-crosstrackError;
crosstrackError = r.y;
sigma += crosstrackError;
if (i > steps) {
error += pow(crosstrackError, 2);
}
// Update robot
r.move(ROBOT_SPEED, -tP*crosstrackError - tD*diff - tI*sigma);
}
return error;
};
draw = function() {
drawWorld();
// Use A* to find a path to the goal
if (mode === PLAN) {
// Draw explored
fill(EXPLORED_COLOR);
noStroke();
for (var i = 0; i < explored.length; i++) {
for (var j = 0; j < explored[0].length; j++) {
if (explored[i][j]) {
rect(j*BLOCK_SIZE, i*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
}
}
// Draw frontier
fill(FRONTIER_COLOR);
noStroke();
for (var i = 0; i < frontier.length; i++) {
rect(frontier[i][0]*BLOCK_SIZE, frontier[i][1]*BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
// A*
if (frontier.length > 0) {
// Remove a node from the frontier
var x = frontier[0][0];
var y = frontier[0][1];
var cost = frontier[0][2];
//println(cost + ", " + frontier[0][3]);
frontier.splice(0, 1);
// Goal check
if (world[y][x] === GOAL) {
mode = SMOOTH;
}
else {
// Add all adjacent unexplored nodes
for (var i = 0; i < DELTA.length; i++) {
// If the new position is in the world
var x2 = x + DELTA[i][0];
var y2 = y + DELTA[i][1];
if (x2 >= 0 && x2 < world[0].length && y2 >= 0 && y2 < world.length) {
// If the position is unexplored
if (!explored[y2][x2] && world[y2][x2] !== 1) {
explored[y2][x2] = true;
predecessors[y2][x2] = [x, y];
addToFrontier([x2, y2, cost+1, dist(x2, y2, GOAL_X, GOAL_Y)]);
}
}
}
}
}
else {
mode = -1;
println("No possible path to goal.");
}
}
// Smooth the path
else if (mode === SMOOTH) {
// Build path
if (path.length === 0) {
var x = GOAL_X;
var y = GOAL_Y;
path.splice(0, 0, [x, y]);
smoothPath.splice(0, 0, [x, y]);
while (x !== 0 || y !== 0) {
var newX = predecessors[y][x][0];
var newY = predecessors[y][x][1];
x = newX;
y = newY;
path.splice(0, 0, [x, y]);
smoothPath.splice(0, 0, [x, y]);
}
}
// Draw the original path
stroke(PATH_COLOR);
strokeWeight(5);
for (var i = 0; i < path.length; i++) {
point(BLOCK_SIZE*path[i][0]+BLOCK_SIZE/2, BLOCK_SIZE*path[i][1]+BLOCK_SIZE/2);
}
strokeWeight(1);
for (var i = 0; i < path.length-1; i++) {
line(BLOCK_SIZE*path[i][0]+BLOCK_SIZE/2, BLOCK_SIZE*path[i][1]+BLOCK_SIZE/2, BLOCK_SIZE*path[i+1][0]+BLOCK_SIZE/2, BLOCK_SIZE*path[i+1][1]+BLOCK_SIZE/2);
}
// Draw the new path
stroke(SMOOTH_PATH_COLOR);
strokeWeight(5);
for (var i = 0; i < smoothPath.length; i++) {
point(BLOCK_SIZE*smoothPath[i][0]+BLOCK_SIZE/2, BLOCK_SIZE*smoothPath[i][1]+BLOCK_SIZE/2);
}
strokeWeight(1);
for (var i = 0; i < smoothPath.length-1; i++) {
line(BLOCK_SIZE*smoothPath[i][0]+BLOCK_SIZE/2, BLOCK_SIZE*smoothPath[i][1]+BLOCK_SIZE/2, BLOCK_SIZE*smoothPath[i+1][0]+BLOCK_SIZE/2,
BLOCK_SIZE*smoothPath[i+1][1]+BLOCK_SIZE/2);
}
// Perform gradient descent
var update;
var diff = 0;
for (var i = 1; i < smoothPath.length-1; i++) {
update = [0, 0];
for (var j = 0; j < smoothPath[0].length; j++) {
update[j] += weightData * (path[i][j] - smoothPath[i][j]);
update[j] += weightSmooth * (smoothPath[(i+1)%smoothPath.length][j] + smoothPath[(i-1+smoothPath.length)%smoothPath.length][j] - 2*smoothPath[i][j]);
}
// Simulataneous update
for (var j = 0; j < smoothPath[0].length; j++) {
smoothPath[i][j] += update[j];
diff += abs(update[j]);
}
}
if (diff < 0.000001) {
robot = new Robot(BLOCK_SIZE/2, BLOCK_SIZE/2, BLOCK_SIZE/2);
mode = NAVIGATE;
}
}
else if (mode === CALIBRATE) {
var steps = 100;
var error = simulate(steps);
var dp = [1.0, 1.0, 1.0];
var params = [0, 0, 0];
if (error/steps > 0.04) {
for (var i = 0; i < dp.length; i++) {
params[i] += dp[i];
var newError = simulate(steps);
if (newError < error) {
error = newError;
dp[i] *= 1.1;
}
else {
params[i] -= 2*dp[i];
newError = simulate(steps);
if (newError < error) {
error = newError;
dp[i] *= -1.1;
}
else {
params[i] += dp[i];
dp[i] *= 0.9;
}
}
}
tP = params[0];
tD = params[1];
tI = params[2];
println(error/steps);
}
else {
println(params);
tP = params[0];
tD = params[1];
tI = params[2];
mode = NAVIGATE;
}
}
// Use PID control to follow the path
else if (mode === NAVIGATE) {
// Draw path
stroke(SMOOTH_PATH_COLOR);
strokeWeight(5);
for (var i = 0; i < smoothPath.length; i++) {
point(BLOCK_SIZE*smoothPath[i][0]+BLOCK_SIZE/2, BLOCK_SIZE*smoothPath[i][1]+BLOCK_SIZE/2);
}
strokeWeight(1);
for (var i = 0; i < smoothPath.length-1; i++) {
line(BLOCK_SIZE*smoothPath[i][0]+BLOCK_SIZE/2, BLOCK_SIZE*smoothPath[i][1]+BLOCK_SIZE/2, BLOCK_SIZE*smoothPath[i+1][0]+BLOCK_SIZE/2,
BLOCK_SIZE*smoothPath[i+1][1]+BLOCK_SIZE/2);
}
// Draw robot
robot.draw();
// Compute cte
var diff = -cte;
var x1 = smoothPath[index][0]*BLOCK_SIZE+BLOCK_SIZE/2;
var y1 = smoothPath[index][1]*BLOCK_SIZE+BLOCK_SIZE/2;
var x2 = smoothPath[index+1][0]*BLOCK_SIZE+BLOCK_SIZE/2;
var y2 = smoothPath[index+1][1]*BLOCK_SIZE+BLOCK_SIZE/2;
var dx = x2-x1;
var dy = y2-y1;
var d = sqrt(pow(dx, 2) + pow(dy, 2));
var u = ((robot.x-x1)*dx + (robot.y-y1)*dy)/(pow(dx, 2) + pow(dy, 2));
var Px = x1 + d*u*cos(atan2(dy, dx));
var Py = y1 + d*u*sin(atan2(dy, dx));
cte = ((robot.y-y1)*dx-(robot.x-x1)*dy)/(pow(dx, 2) + pow(dy, 2));
sigma += cte;
diff += cte;
if (u > 1) {
index++;
index = min(index, smoothPath.length-2);
}
// Update robot
robot.move(ROBOT_SPEED, -tP*cte - tD*diff - tI*sigma);
//println(index);
}
};
but all that happens is that a grey screen shows up
For a project I am attempting to make my own linear gradient code. And I have managed to come quite far, however, it is not perfect. Some places the gradient is not looking as it should.
See the following code where the colorStops are the stops in the gradient. It is supposed to show a gradient in between red, yellow and green as in a traffic light.
gradientSlider = {
colorStops: ["#b30000", "#ffff1a", "#00e600"],
minValue: 0,
maxValue: 100,
init: function(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.width = canvas.width;
this.height = canvas.height;
this.colors = this.calculateHexColorForStep();
this.draw();
},
draw: function() {
pixelWidth = this.width / this.maxValue;
for (i = 0; i < this.maxValue; i++) {
this.ctx.beginPath();
this.ctx.rect(i * pixelWidth, 0, pixelWidth, this.height);
this.ctx.fillStyle = this.colors[i];
this.ctx.fill();
}
},
calculateHexColorForStep: function() {
result = [];
stepsPerGradient = this.maxValue / (this.colorStops.length - 1);
for (i = 0; i < this.colorStops.length - 1; i++) {
percentIncrease = 100 / stepsPerGradient / 100;
firstColor = this.colorStops[i];
targetColor = this.colorStops[i + 1];
firstColorDecArray = this.tools.parseColor(firstColor);
targetColorDecArray = this.tools.parseColor(targetColor);
for (j = 0; j <= stepsPerGradient; j++) {
if (j == 0) {
result.push(firstColor);
} else if (j == stepsPerGradient) {
result.push(targetColor);
} else {
stepColorDecArray = [firstColorDecArray[0] + (percentIncrease * j) * (targetColorDecArray[0] - firstColorDecArray[0]),
firstColorDecArray[1] + (percentIncrease * j) * (targetColorDecArray[1] - firstColorDecArray[1]),
firstColorDecArray[2] + (percentIncrease * j) * (targetColorDecArray[2] - firstColorDecArray[2])
];
result.push(this.tools.decimalToHex(stepColorDecArray));
}
}
}
return result;
},
tools: {
parseColor: function(hexColorString) {
var m;
m = hexColorString.match(/^#([0-9a-f]{6})$/i)[1];
if (m) {
return [parseInt(m.substring(0, 2), 16), parseInt(m.substring(2, 4), 16), parseInt(m.substring(4, 6), 16)];
}
},
decimalToHex: function(decimalNumberArray) {
//TODO fikse tall under ti - alltid to plasser
var results = [];
results[1] = Math.round(decimalNumberArray[0]).toString(16);
results[2] = Math.round(decimalNumberArray[1]).toString(16);
results[3] = Math.round(decimalNumberArray[2]).toString(16);
for (var i = 1; i <= results.length; i++) {
if (!(isNaN(results[i]))) {
if (results[i] < 10) {
results[i] = "0" + results[i];
}
}
}
return "#" + results[1] + results[2] + results[3];
}
}
}
//get the canvas element
var canvasElm = document.querySelector("canvas");
//initialize the slider
gradientSlider.init(canvasElm);
<canvas id="gradient-slider" width="600" height="200" class="gradientslider"></canvas>
My solution is a fixed version of the function tools.decimalToHex. The error was that you treat the results array as an array of numbers, instead of an array of strings.
decimalToHex : function(decimalNumberArray){
var results = [];
// Maybe check if number is in range 0 - 255, before converting to string?
results[0] = Math.round(decimalNumberArray[0]).toString(16);
results[1] = Math.round(decimalNumberArray[1]).toString(16);
results[2] = Math.round(decimalNumberArray[2]).toString(16);
for (var i = 0; i<results.length; i++) {
if(results[i].length < 2) {
results[i] = "0" + results[i];
}
}
return "#" + results[0] + results[1] + results[2];
}
Corresponding jsFiddle: https://jsfiddle.net/8xr4zujj/4/