Strategy to optimize javascript - javascript

I have written a javascript program that uses a genetic algorithm to recreate an image only using triangles. Here's the strategy:
generate a random pool of models, each model having an array of triangles (3 points and a color)
evaluate the fitness of each model. To do so, I compare the original image's pixel array with my model's. I use Cosine Similarity to compare arrays
keep the best models, and mate them to create new models
randomly mutate some of the models
evaluate the new pool and continue
It works quite well after some iterations as you can see here:
The problem I have, is that it is very slow, most of the time is spent getting model's pixels (converting list of triangles (color + points) to a pixel array).
Here's how I do so now:
My pixel-array is a 1D array, I need to be able to convert x,y coordinates to index:
static getIndex(x, y, width) {
return 4 * (width * y + x);
}
Then I am able to draw a point:
static plot(x, y, color, img) {
let idx = this.getIndex(x, y, img.width);
let added = [color.r, color.g, color.b, map(color.a, 0, 255, 0, 1)];
let base = [img.pixels[idx], img.pixels[idx + 1], img.pixels[idx + 2], map(img.pixels[idx + 3], 0, 255, 0, 1)];
let a01 = 1 - (1 - added[3]) * (1 - base[3]);
img.pixels[idx + 0] = Math.round((added[0] * added[3] / a01) + (base[0] * base[3] * (1 - added[3]) / a01)); // red
img.pixels[idx + 1] = Math.round((added[1] * added[3] / a01) + (base[1] * base[3] * (1 - added[3]) / a01)); // green
img.pixels[idx + 2] = Math.round((added[2] * added[3] / a01) + (base[2] * base[3] * (1 - added[3]) / a01)); // blue
img.pixels[idx + 3] = Math.round(map(a01, 0, 1, 0, 255));
}
Then a line:
static line(x0, y0, x1, y1, img, color) {
x0 = Math.round(x0);
y0 = Math.round(y0);
x1 = Math.round(x1);
y1 = Math.round(y1);
let dx = Math.abs(x1 - x0);
let dy = Math.abs(y1 - y0);
let sx = x0 < x1 ? 1 : -1;
let sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
do {
this.plot(x0, y0, color, img);
let e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
} while (x0 != x1 || y0 != y1);
}
And finally, a triangle:
static drawTriangle(triangle, img) {
for (let i = 0; i < triangle.points.length; i++) {
let point = triangle.points[i];
let p1 =
i === triangle.points.length - 1
? triangle.points[0]
: triangle.points[i + 1];
this.line(point.x, point.y, p1.x, p1.y, img, triangle.color);
}
this.fillTriangle(triangle, img);
}
static fillTriangle(triangle, img) {
let vertices = Array.from(triangle.points);
vertices.sort((a, b) => a.y > b.y);
if (vertices[1].y == vertices[2].y) {
this.fillBottomFlatTriangle(vertices[0], vertices[1], vertices[2], img, triangle.color);
} else if (vertices[0].y == vertices[1].y) {
this.fillTopFlatTriangle(vertices[0], vertices[1], vertices[2], img, triangle.color);
} else {
let v4 = {
x: vertices[0].x + float(vertices[1].y - vertices[0].y) / float(vertices[2].y - vertices[0].y) * (vertices[2].x - vertices[0].x),
y: vertices[1].y
};
this.fillBottomFlatTriangle(vertices[0], vertices[1], v4, img, triangle.color);
this.fillTopFlatTriangle(vertices[1], v4, vertices[2], img, triangle.color);
}
}
static fillBottomFlatTriangle(v1, v2, v3, img, color) {
let invslope1 = (v2.x - v1.x) / (v2.y - v1.y);
let invslope2 = (v3.x - v1.x) / (v3.y - v1.y);
let curx1 = v1.x;
let curx2 = v1.x;
for (let scanlineY = v1.y; scanlineY <= v2.y; scanlineY++) {
this.line(curx1, scanlineY, curx2, scanlineY, img, color);
curx1 += invslope1;
curx2 += invslope2;
}
}
static fillTopFlatTriangle(v1, v2, v3, img, color) {
let invslope1 = (v3.x - v1.x) / (v3.y - v1.y);
let invslope2 = (v3.x - v2.x) / (v3.y - v2.y);
let curx1 = v3.x;
let curx2 = v3.x;
for (let scanlineY = v3.y; scanlineY > v1.y; scanlineY--) {
this.line(curx1, scanlineY, curx2, scanlineY, img, color);
curx1 -= invslope1;
curx2 -= invslope2;
}
}
You can see full code in action here
So, I would like to know:
is it possible to optimize this code ?
if yes, what would be the best way to do so ? Maybe there is a library doing all of the drawing stuff way better than I did ? Or by using workers ?
Thanks !

I have tested your suggestions, here's the results:
Use RMS instead of Cosine Similarity: I am not sur if the measure of similarity is better, but it is definitively not worse. It seems to run a little bit faster too.
Use UInt8Array: It surely have an impact, but does not runs a lot faster. Not slower though.
Draw to invisible canvas: Definitively faster and easier! I can remove all of my drawing functions and replace it with a few lines of code, and it runs a lot faster !
Here's the code to draw to an invisible canvas:
var canvas = document.createElement('canvas');
canvas.id = "CursorLayer";
canvas.width = this.width;
canvas.height = this.height;
canvas.display = "none";
var body = document.getElementsByTagName("body")[0];
body.appendChild(canvas);
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 1)";
ctx.fillRect(0, 0, this.width, this.height);
for (let i = 0; i < this.items.length; i++) {
let item = this.items[i];
ctx.fillStyle = "rgba(" +item.color.r + ','+item.color.g+','+item.color.b+','+map(item.color.a, 0, 255, 0, 1)+")";
ctx.beginPath();
ctx.moveTo(item.points[0].x, item.points[0].y);
ctx.lineTo(item.points[1].x, item.points[1].y);
ctx.lineTo(item.points[2].x, item.points[2].y);
ctx.fill();
}
let pixels = ctx.getImageData(0, 0, this.width, this.height).data;
//delete canvas
body.removeChild(canvas);
return pixels;
Before those changements, my code were running at about 1.68 iterations per second.
Now it runs at about 16.45 iterations per second !
See full code here.
Thanks again !

Related

Fill in shape with lines at a specified angle

I need to create line segments within a shape and not just a visual pattern - I need to know start and end coordinates for those lines that are within a given boundary (shape). I'll go through what I have and explain the issues I'm facing
I have a closed irregular shape (can have dozens of sides) defined by [x, y] coordinates
shape = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
];
I calculate and draw the bounding box of this shape
I then draw my shape on the canvas
Next, I cast rays within the bounding box with a set spacing between each ray. The ray goes from left to right incrementing by 1 pixel.
Whenever a cast ray gets to a pixel with RGB values of 100, 255, 100 I then know it has entered the shape. I know when it exits the shape if the pixel value is not 100, 255, 100. Thus I know start and end coordinates for each line within my shape and if one ray enters and exits the shape multiple times - this will generate all line segments within that one ray cast.
For the most part it works but there are issues:
It's very slow. Perhaps there is a better way than casting rays? Or perhaps there is a way to optimize the ray logic? Perhaps something more intelligent than just checking for RGB color values?
How do I cast rays at a different angle within the bounding box? Now it's going left to right, but how would I fill my bounding box with rays cast at any specified angle? i.e.:
I don't care about holes or curves. The shapes will all be made of straight line segments and won't have any holes inside them.
Edit: made changes to the pixel RGB sampling that improve performance.
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 15;
shape = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
];
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
// display bounding box
ctx.fillStyle = 'rgba(255,0,0,.2)';
ctx.fillRect(boundingBox[0][0], boundingBox[0][1], boundingBox[1][0]-boundingBox[0][0], boundingBox[1][1]-boundingBox[0][1]);
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
canvasData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
// loop through the shape in vertical slices
for(var i = boundingBox[0][1]+lineSpacing; i <= boundingBox[1][1]; i += lineSpacing) {
// send ray from left to right
for(var j = boundingBox[0][0], start = false; j <= boundingBox[1][0]; j++) {
x = j, y = i;
pixel = y * (canvas.width * 4) + x * 4;
// if pixel is within boundary (shape)
if(canvasData[pixel] == 100 && canvasData[pixel+1] == 255 && canvasData[pixel+2] == 100) {
// arrived at start of boundary
if(start === false) {
start = [x,y]
}
} else {
// arrived at end of boundary
if(start !== false) {
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
ctx.lineTo(x, y);
ctx.closePath();
ctx.stroke();
start = false;
}
}
}
// show entire cast ray for debugging purposes
ctx.strokeStyle = 'rgba(0,0,0,.2)';
ctx.beginPath();
ctx.moveTo(boundingBox[0][0], i);
ctx.lineTo(boundingBox[1][0], i);
ctx.closePath();
ctx.stroke();
}
<canvas id="canvas" width="350" height="350"></canvas>
This is a pretty complex problem that I am trying to simplify as much as possible. Using the line intersection formula we can determin where the ray intersects with the shape at every edge. What we can do is loop through each side of the shape while check every rays intersection. If they intersect we push those coordinates to an array.
I have tried to make this as dynamic as possible. You can pass the shape and change the number of rays and the angle. As for the angle it doesn't take a specific degree (i.e. 45) but rather you change the start and stop y axis. I'm sure if you must have the ability to put in a degree we can do that.
It currently console logs the array of intersecting coordinates but you can output them however you see fit.
The mouse function is just to verify that the number match up. Also be aware I am using toFixed() to get rid of lots of decimals but it does convert to a string. If you need an integer you'll have to convert back.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d")
canvas.width = 300;
canvas.height = 300;
ctx.fillStyle = "violet";
ctx.fillRect(0,0,canvas.width,canvas.height)
//Shapes
let triangleish = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
]
let star = [ [ 0, 85 ], [ 75, 75 ], [ 100, 10 ], [ 125, 75 ],
[ 200, 85 ], [ 150, 125 ], [ 160, 190 ], [ 100, 150 ],
[ 40, 190 ], [ 50, 125 ], [ 0, 85 ] ];
let coords = [];
//Class that draws the shape on canvas
function drawShape(arr) {
ctx.beginPath();
ctx.fillStyle = "rgb(0,255,0)";
ctx.moveTo(arr[0][0], arr[0][1]);
for (let i=1;i<arr.length;i++) {
ctx.lineTo(arr[i][0], arr[i][1]);
}
ctx.fill();
ctx.closePath();
}
//pass the shape in here to draw it
drawShape(star)
//Class to creat the rays.
class Rays {
constructor(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.w = canvas.width;
this.h = 1;
}
draw() {
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.moveTo(this.x1, this.y1)
ctx.lineTo(this.x2, this.y2)
ctx.stroke();
ctx.closePath();
}
}
let rays = [];
function createRays(angle) {
let degrees = angle * (Math.PI/180)
//I am currently creating an array every 10px on the Y axis
for (let i=0; i < angle + 45; i++) {
//The i will be your start and stop Y axis. This is where you can change the angle
let cx = canvas.width/2 + (angle*2);
let cy = i * 10;
let x1 = (cx - 1000 * Math.cos(degrees));
let y1 = (cy - 1000 * Math.sin(degrees));
let x2 = (cx + 1000 * Math.cos(degrees));
let y2 = (cy + 1000 * Math.sin(degrees));
rays.push(new Rays(x1, y1, x2, y2))
}
}
//enter angle here
createRays(40);
//function to draw the rays after crating them
function drawRays() {
for (let i=0;i<rays.length; i++) {
rays[i].draw();
}
}
drawRays();
//This is where the magic happens. Using the line intersect formula we can determine if the rays intersect with the objects sides
function intersectLines(coord1, coord2, rays) {
let x1 = coord1[0];
let x2 = coord2[0];
let y1 = coord1[1];
let y2 = coord2[1];
let x3 = rays.x1;
let x4 = rays.x2;
let y3 = rays.y1;
let y4 = rays.y2;
//All of this comes from Wikipedia on line intersect formulas
let d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) {
return
}
let t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
let u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
//if this statement is true then the lines intersect
if (t > 0 && t < 1 && u > 0) {
//I have currently set it to fixed but if a string does not work for you you can change it however you want.
//the first formula is the X coord of the interect the second is the Y
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return
}
//function to call the intersect function by passing in the shapes sides and each ray
function callIntersect(shape) {
for (let i=0;i<shape.length;i++) {
for (let j=0;j<rays.length;j++) {
if (i < shape.length - 1) {
intersectLines(shape[i], shape[i+1], rays[j]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rays[j]);
}
}
}
}
callIntersect(star);
//just to sort them by the Y axis so they they show up as in-and-out
function sortCoords() {
coords.sort((a, b) => {
return a[1] - b[1];
});
}
sortCoords()
console.log(coords)
//This part is not needed only added to verify number matched the mouse posit
let mouse = {
x: undefined,
y: undefined
}
let canvasBounds = canvas.getBoundingClientRect();
addEventListener('mousemove', e => {
mouse.x = e.x - canvasBounds.left;
mouse.y = e.y - canvasBounds.top;
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawCoordinates();
})
function drawCoordinates() {
ctx.font = '15px Arial';
ctx.fillStyle = 'black';
ctx.fillText('x: '+mouse.x+' y: '+mouse.y, mouse.x, mouse.y)
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "violet";
ctx.fillRect(0,0,canvas.width,canvas.height)
for (let i=0;i<rays.length; i++) {
rays[i].draw();
}
drawShape(star)
drawCoordinates();
requestAnimationFrame(animate)
}
animate()
<canvas id="canvas"></canvas>
I'm not an expert, but maybe you could do something like this:
Generate the points that constitute the borders.
Organize them in a convenient structure, e.g. an object with the y as key, and an array of x as values.
2.1. i.e. each item in the object would constitute all points of all borders in a single y.
Iterate over the object and generate the segments for each y.
3.1. e.g. if the array of y=12 contains [ 10, 20, 60, 80 ] then you would generate two segments: [ 10, 12 ] --> [ 20, 12 ] and [ 60, 12 ] --> [ 80, 12 ].
To generate the borders' points (and to answer your second question), you can use the line function y = a*x + b.
For example, to draw a line between [ 10, 30 ] and [ 60, 40 ], you would:
Solve a and b by substituting x and y for both points and combining these two formulas (with standard algebra):
For point #1: 30 = a*10 + b
For point #2: 40 = a*60 + b
b = 30 - a*10
40 = a*60 + (30 - a*10)
a*60 - a*10 = 40 - 30
50*a = 10
a = 0.2
30 = a*10 + b
30 = 0.2*10 + b
b = 30 - 2
b = 28
With a and b at hand, you get the function for your specific line:
y = 0.2*x + 28
With that, you can calculate the point of the line for any y. So, for example, the x of the point right under the first point ([ 10, 30 ]) would have a y of 31, and so: 31 = 0.2*x + 28, and so: x = 15. So you get: [ 15, 31 ].
You may need a bit of special handling for:
Vertical lines, because the slope is "infinite" and calculating it would cause division by zero.
Rounding issues. For some (probably most) pixels you will get real x values (i.e. non-integer). You can Math.round() them, but it can cause issues, like:
8.1. Diagonal rays may not actually hit a border point even when they go through a border. This will probably require additional handling (like checking points around and not just exactly the pixels the ray lies on).
8.2. The points your algorithm generate may (slightly) differ from the points that appear on the screen when you use libraries or built-in browser functionality to draw the shape (depending on the implementation of their drawing algorithms).
This is a mashup of Justin's answer and code from my proposed question.
One issue was generating rays at a set angle and a set distance from each other. To have rays be equal distances apart at any angle we can use a vector at a 90 degree angle and then place a new center point for the next line.
We can start at the exact midpoint of our boundary and then spread out on either side.
Red line is the center line, green dots are the vector offset points for the next line.
Next I modified Justin's intersect algorithm to iterate by ray and not side, that way I get interlaced coordinates where array[index] is the start point of a segment and array[index+1] is the end point.
And by connecting the lines we get a shape that is filled with lines inside its boundaries at set distances apart
Issues:
I had to inflate the boundary by 1 pixel otherwise certain shapes would fail to generate paths
I'd like rays to be some what aligned. It's hard to explain, but here's an example of 6 triangles rotated at 60 degree increments that form a hexagon with their inner lines also offset by 60 degree increments. The top and bottom triangle inner lines do not join those of the outside triangles. This is an issue with the cast rays. Ideally I'd like them to join and be aligned with the outer most edge if that makes sense. Surely there is a better way to cast rays than this...
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 12;
angle = 45;
shapes = [
[[143.7,134.2], [210.4,18.7], [77.1,18.7]],
[[143.7,134.2], [77.1,18.7], [10.4,134.2]],
[[143.7,134.2], [10.4,134.2], [77.1,249.7]],
[[143.7,134.2], [77.1,249.7], [210.4,249.7]],
[[143.7,134.2], [210.4,249.7], [277.1,134.2]],
[[143.7,134.2], [277.1,134.2], [210.4,18.7]]
];
for(var i in shapes) {
lines = getLineSegments(shapes[i], 90+(-60*i), lineSpacing);
for(var i = 0; i < lines.length; i += 2) {
start = lines[i];
end = lines[i+1];
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
ctx.closePath();
ctx.stroke();
}
}
function getLineSegments(shape, angle, lineSpacing) {
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
boundingBox[0][0] -= 1, boundingBox[0][1] -= 1;
boundingBox[1][0] += 1, boundingBox[1][1] += 1;
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
rayPaths = [];
path = getPathCoords(boundingBox, 0, 0, angle);
rayPaths.push(path);
/*ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
getPaths:
for(var i = 0, lastPaths = [path, path]; true; i++) {
for(var j = 0; j < 2; j++) {
pathMidX = (lastPaths[j][0][0] + lastPaths[j][1][0]) / 2;
pathMidY = (lastPaths[j][0][1] + lastPaths[j][1][1]) / 2;
pathVectorX = lastPaths[j][1][1] - lastPaths[j][0][1];
pathVectorY = lastPaths[j][1][0] - lastPaths[j][0][0];
pathLength = Math.sqrt(pathVectorX * pathVectorX + pathVectorY * pathVectorY);
pathOffsetPointX = pathMidX + ((j % 2 === 0 ? pathVectorX : -pathVectorX) / pathLength * lineSpacing);
pathOffsetPointY = pathMidY + ((j % 2 === 0 ? -pathVectorY : pathVectorY) / pathLength * lineSpacing);
offsetX = pathOffsetPointX-boundingMidX;
offsetY = pathOffsetPointY-boundingMidY;
path = getPathCoords(boundingBox, offsetX, offsetY, angle);
if(
path[0][0] < boundingBox[0][0] ||
path[1][0] > boundingBox[1][0] ||
path[0][0] > boundingBox[1][0] ||
path[1][0] < boundingBox[0][0]
) break getPaths;
/*ctx.fillStyle = 'green';
ctx.fillRect(pathOffsetPointX-2.5, pathOffsetPointY-2.5, 5, 5);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
rayPaths.push(path);
lastPaths[j] = path;
}
}
coords = [];
function intersectLines(coord1, coord2, rays) {
x1 = coord1[0], x2 = coord2[0];
y1 = coord1[1], y2 = coord2[1];
x3 = rays[0][0], x4 = rays[1][0];
y3 = rays[0][1], y4 = rays[1][1];
d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) return;
t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
if (t > 0 && t < 1 && u > 0) {
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return;
}
function callIntersect(shape) {
for (var i = 0; i < rayPaths.length; i++) {
for (var j = 0; j< shape.length; j++) {
if (j < shape.length - 1) {
intersectLines(shape[j], shape[j+1], rayPaths[i]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rayPaths[i]);
}
}
}
}
callIntersect(shape);
return coords;
}
function getPathCoords(boundingBox, offsetX, offsetY, angle) {
coords = [];
// add decimal places otherwise can lead to Infinity, subtract 90 so 0 degrees is at the top
angle = angle + 0.0000000000001 - 90;
boundingBoxWidth = boundingBox[1][0] - boundingBox[0][0];
boundingBoxHeight = boundingBox[1][1] - boundingBox[0][1];
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
x = boundingMidX + offsetX, y = boundingMidY + offsetY;
dx = Math.cos(Math.PI * angle / 180);
dy = Math.sin(Math.PI * angle / 180);
for(var i = 0; i < 2; i++) {
bx = (dx > 0) ? boundingBoxWidth+boundingBox[0][0] : boundingBox[0][0];
by = (dy > 0) ? boundingBoxHeight+boundingBox[0][1] : boundingBox[0][1];
if(dx == 0) ix = x, iy = by;
if(dy == 0) iy = y, ix = bx;
tx = (bx - x) / dx;
ty = (by - y) / dy;
if(tx <= ty) {
ix = bx, iy = y + tx * dy;
} else {
iy = by, ix = x + ty * dx;
}
coords.push([ix, iy]);
dx = -dx;
dy = -dy;
}
return coords;
}
<canvas id="canvas" width="500" height="500"></canvas>
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 10;
angle = 45;
shape = [
[200,10], // x, y
[10,300],
[200,200],
[400,300]
];
lines = getLineSegments(shape, angle, lineSpacing);
for(var i = 0; i < lines.length; i += 2) {
start = lines[i];
end = lines[i+1];
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
ctx.closePath();
ctx.stroke();
}
function getLineSegments(shape, angle, lineSpacing) {
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
boundingBox[0][0] -= 1, boundingBox[0][1] -= 1;
boundingBox[1][0] += 1, boundingBox[1][1] += 1;
// display bounding box
ctx.fillStyle = 'rgba(255,0,0,.2)';
ctx.fillRect(boundingBox[0][0], boundingBox[0][1], boundingBox[1][0]-boundingBox[0][0], boundingBox[1][1]-boundingBox[0][1]);
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
rayPaths = [];
path = getPathCoords(boundingBox, 0, 0, angle);
rayPaths.push(path);
/*ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
getPaths:
for(var i = 0, lastPaths = [path, path]; true; i++) {
for(var j = 0; j < 2; j++) {
pathMidX = (lastPaths[j][0][0] + lastPaths[j][1][0]) / 2;
pathMidY = (lastPaths[j][0][1] + lastPaths[j][1][1]) / 2;
pathVectorX = lastPaths[j][1][1] - lastPaths[j][0][1];
pathVectorY = lastPaths[j][1][0] - lastPaths[j][0][0];
pathLength = Math.sqrt(pathVectorX * pathVectorX + pathVectorY * pathVectorY);
pathOffsetPointX = pathMidX + ((j % 2 === 0 ? pathVectorX : -pathVectorX) / pathLength * lineSpacing);
pathOffsetPointY = pathMidY + ((j % 2 === 0 ? -pathVectorY : pathVectorY) / pathLength * lineSpacing);
offsetX = pathOffsetPointX-boundingMidX;
offsetY = pathOffsetPointY-boundingMidY;
path = getPathCoords(boundingBox, offsetX, offsetY, angle);
if(
path[0][0] < boundingBox[0][0] ||
path[1][0] > boundingBox[1][0] ||
path[0][0] > boundingBox[1][0] ||
path[1][0] < boundingBox[0][0]
) break getPaths;
/*ctx.fillStyle = 'green';
ctx.fillRect(pathOffsetPointX-2.5, pathOffsetPointY-2.5, 5, 5);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
rayPaths.push(path);
lastPaths[j] = path;
}
}
coords = [];
function intersectLines(coord1, coord2, rays) {
x1 = coord1[0], x2 = coord2[0];
y1 = coord1[1], y2 = coord2[1];
x3 = rays[0][0], x4 = rays[1][0];
y3 = rays[0][1], y4 = rays[1][1];
d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) return;
t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
if (t > 0 && t < 1 && u > 0) {
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return;
}
function callIntersect(shape) {
for (var i = 0; i < rayPaths.length; i++) {
for (var j = 0; j< shape.length; j++) {
if (j < shape.length - 1) {
intersectLines(shape[j], shape[j+1], rayPaths[i]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rayPaths[i]);
}
}
}
}
callIntersect(shape);
return coords;
}
function getPathCoords(boundingBox, offsetX, offsetY, angle) {
coords = [];
// add decimal places otherwise can lead to Infinity, subtract 90 so 0 degrees is at the top
angle = angle + 0.0000000000001 - 90;
boundingBoxWidth = boundingBox[1][0] - boundingBox[0][0];
boundingBoxHeight = boundingBox[1][1] - boundingBox[0][1];
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
x = boundingMidX + offsetX, y = boundingMidY + offsetY;
dx = Math.cos(Math.PI * angle / 180);
dy = Math.sin(Math.PI * angle / 180);
for(var i = 0; i < 2; i++) {
bx = (dx > 0) ? boundingBoxWidth+boundingBox[0][0] : boundingBox[0][0];
by = (dy > 0) ? boundingBoxHeight+boundingBox[0][1] : boundingBox[0][1];
if(dx == 0) ix = x, iy = by;
if(dy == 0) iy = y, ix = bx;
tx = (bx - x) / dx;
ty = (by - y) / dy;
if(tx <= ty) {
ix = bx, iy = y + tx * dy;
} else {
iy = by, ix = x + ty * dx;
}
coords.push([ix, iy]);
dx = -dx;
dy = -dy;
}
return coords;
}
<canvas id="canvas" width="500" height="500"></canvas>

How to draw arc on canvas HTML5 without interpolation?

I am using the following code to draw on HTML5 canvas:
const context = canvas.getContext('2d');
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
context.closePath();
However, if I print unique values:
console.log(new Set(context.getImageData(0, 0, canvas.width, canvas.height).data))
I can see that the color that I use in fillStyle gets interpolated.
I tried to disable interpolation/smoothing by adding the following flags:
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
However, it does not help. I would highly appreciate if you could advise me how to fix the issue.
The is no native way to draw circles that are pixelated. To do that you must render each pixel manually.
There are several methods you can use to do this. The most common have some additional artifacts (like inconsistent line width) that are hard to avoid.
The following function draw a circle using a modification of the Berzingham line algorithm (also good for rendering pixelated lines) called the Midpoint circle algorithm
Unfortunately most of the methods that can draw arbitrary lines and circle are slow. The two mentioned above are the fastest standard methods I know about.
Example
The example defines 3 functions to draw pixelated circles
pixelPixelatedCircle (Red outer circles and single blue in example) draws a single pixel wide circle using the current fill style
fillPixelatedCircle (Red inner circle in example) draws a a solid circle using the current fill style
strokePixelatedCircle (Black circles in example) draws a circle line with a width. Not the width only works when it is >= 2. If you want a single pixel width use the first function. Also not that this function uses a second canvas to render the circle
The example draws all three types
The outer red circle drawn using pixelPixelatedCircle are to demonstrate that the quality of the circles are consistent. There should be alternating 1 pixel width circles, red and dark red. and an outer blue just touching the canvas edge circles.
For circles less than radius of 2 use ctx.rect as the outcome will be the same.
Note the circle radius is an integer thus a circle radius 1000 will be identical to circle radius 1000.9 The sample applies to the circle center. To be able to have sub pixel positioning and radius will need another algorithm which is slower and has lower quality lines.
Note I added a simple zoom canvas so I could see the results better, I was going to remove it but left it in just for interested people. It is not crucial to the answer.
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const size = Math.min(w, h);
const circleWorkCanvas = document.createElement("canvas");
const cCtx = circleWorkCanvas.getContext("2d");
function resizeCircleCanvas(ctx) {
if (circleWorkCanvas.width !== ctx.canvas.width || circleWorkCanvas.height !== ctx.canvas.height) {
circleWorkCanvas.width = ctx.canvas.width;
circleWorkCanvas.height = ctx.canvas.height;
}
}
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.35, 5);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.3, 4);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.25, 3);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.2, 2);
ctx.fillStyle = "red";
fillPixelatedCircle(ctx, w / 2, h / 2, size * 0.15);
ctx.fillStyle = "blue";
pixelPixelatedCircle(ctx, w / 2, h / 2, size * 0.38);
ctx.fillStyle = "blue";
pixelPixelatedCircle(ctx, w / 2, h / 2, size * 0.5);
ctx.fillStyle = "red";
for(let v = 0.40; v < 0.49; v += 1 / size) {
ctx.fillStyle = "#600"
pixelPixelatedCircle(ctx, w / 2, h / 2, size * v);
ctx.fillStyle = "#F00"
v += 1 / size;
pixelPixelatedCircle(ctx, w / 2, h / 2, size * v );
}
function strokePixelatedCircle(ctx, cx, cy, r, lineWidth) {
resizeCircleCanvas(ctx);
cCtx.clearRect(0, 0, cCtx.canvas.width, cCtx.canvas.height);
cCtx.globalCompositeOperation = "source-over";
cCtx.fillStyle = ctx.strokeStyle;
fillPixelatedCircle(cCtx, cx, cy, r + lineWidth / 2);
cCtx.globalCompositeOperation = "destination-out";
fillPixelatedCircle(cCtx, cx, cy, r - lineWidth / 2);
cCtx.globalCompositeOperation = "source-over";
ctx.drawImage(cCtx.canvas, 0, 0);
}
function fillPixelatedCircle(ctx, cx, cy, r){
r |= 0; // floor radius
ctx.setTransform(1,0,0,1,0,0); // ensure default transform
var x = r, y = 0, dx = 1, dy = 1;
var err = dx - (r << 1);
var x0 = cx - 1| 0, y0 = cy | 0;
var lx = x,ly = y;
ctx.beginPath();
while (x >= y) {
ctx.rect(x0 - x, y0 + y, x * 2 + 2, 1);
ctx.rect(x0 - x, y0 - y, x * 2 + 2, 1);
if (x !== lx){
ctx.rect(x0 - ly, y0 - lx, ly * 2 + 2, 1);
ctx.rect(x0 - ly, y0 + lx, ly * 2 + 2, 1);
}
lx = x;
ly = y;
y++;
err += dy;
dy += 2;
if (err > 0) {
x--;
dx += 2;
err += (-r << 1) + dx;
}
}
if (x !== lx) {
ctx.rect(x0 - ly, y0 - lx, ly * 2 + 1, 1);
ctx.rect(x0 - ly, y0 + lx, ly * 2 + 1, 1);
}
ctx.fill();
}
function pixelPixelatedCircle(ctx, cx, cy, r){
r |= 0;
ctx.setTransform(1,0,0,1,0,0); // ensure default transform
var x = r, y = 0, dx = 1, dy = 1;
var err = dx - (r << 1);
var x0 = cx | 0, y0 = cy | 0;
var lx = x,ly = y;
var w = 1, px = x0;
ctx.beginPath();
var rendering = 2;
while (rendering) {
const yy = y0 - y;
const yy1 = y0 + y - 1;
const xx = x0 - x;
const xx1 = x0 + x - 1;
ctx.rect(xx, yy1, 1, 1);
ctx.rect(xx, yy, 1, 1);
ctx.rect(xx1, yy1, 1, 1);
ctx.rect(xx1, yy, 1, 1);
if (x !== lx){
const yy = y0 - lx;
const yy1 = y0 + lx - 1;
const xx = x0 - ly;
w = px - xx;
const xx1 = x0 + ly - w;
ctx.rect(xx, yy, w, 1);
ctx.rect(xx, yy1, w, 1);
ctx.rect(xx1, yy, w, 1);
ctx.rect(xx1, yy1, w, 1);
px = xx;
}
lx = x;
ly = y;
y++;
err += dy;
dy += 2;
if (err > 0) {
x--;
dx += 2;
err += (-r << 1) + dx;
}
if (x < y) { rendering -- }
}
ctx.fill();
}
const ctxZ = canvasZoom.getContext("2d");
canvas.addEventListener("mousemove",(event) => {
ctxZ.clearRect(0,0,30,30);
ctxZ.drawImage(canvas, -(event.pageX-10), -(event.pageY-10));
});
canvas {border: 1px solid black}
#canvasZoom {
width: 300px;
height: 300px;
image-rendering: pixelated;
}
<canvas id="canvas" width="300" height="300"></canvas>
<canvas id="canvasZoom" width="30" height="30"></canvas>
There doesn't appear to be a built-in setting that I can find, but you can loop through the image data and set the individual pixels if they are within some threshold of what you want.
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.beginPath();
context.arc(250, 250, 250, 0, 2 * Math.PI, false);
context.fillStyle = 'rgb(255, 0, 0)';
context.fill();
context.closePath();
console.log(getDistinctColors(context).length + " distinct colors before filter");
solidifyColor(context, 255, 0, 0);
console.log(getDistinctColors(context).length + " distinct colors aftrer filter");
function solidifyColor(context, r, g, b, threshold = 3) {
const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
var rDif = Math.abs(imageData.data[i + 0] - r);
var bDif = Math.abs(imageData.data[i + 1] - b);
var gDif = Math.abs(imageData.data[i + 2] - g);
if (rDif <= threshold && bDif <= threshold && gDif <= threshold) {
imageData.data[i + 0] = r;
imageData.data[i + 1] = g;
imageData.data[i + 2] = b;
imageData.data[i + 3] = 255; // remove alpha
}
}
context.putImageData(imageData, 0, 0);
}
function getDistinctColors(context) {
var colors = [];
const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
colors.push([
imageData.data[i + 0], // R value
imageData.data[i + 1], // G value
imageData.data[i + 2], // B value
imageData.data[i + 3] // A value
]);
}
return [...new Set(colors.map(a => JSON.stringify(a)))].map(a => JSON.parse(a));
}
<canvas id=canvas width=500 height=500></canvas>

Using a line to divide a canvas into two new canvases

I'm looking to allow users to slice an existing canvas into two canvases in whatever direction they would like.
I know how to allow the user to draw a line and I also know how to copy the image data of one canvas onto two new ones, but how can I copy only the relevant color data on either side of the user-drawn line to its respective canvas?
For example, in the following demo I'd like the canvas to be "cut" where the white line is:
const canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d");
const red = "rgb(104, 0, 0)",
lb = "rgb(126, 139, 185)",
db = "rgb(20, 64, 87)";
var width,
height,
centerX,
centerY,
smallerDimen;
var canvasData,
inCoords;
function sizeCanvas() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
centerX = width / 2;
centerY = height / 2;
smallerDimen = Math.min(width, height);
}
function drawNormalState() {
// Color the bg
ctx.fillStyle = db;
ctx.fillRect(0, 0, width, height);
// Color the circle
ctx.arc(centerX, centerY, smallerDimen / 4, 0, Math.PI * 2, true);
ctx.fillStyle = red;
ctx.fill();
ctx.lineWidth = 3;
ctx.strokeStyle = lb;
ctx.stroke();
// Color the triangle
ctx.beginPath();
ctx.moveTo(centerX + smallerDimen / 17, centerY - smallerDimen / 10);
ctx.lineTo(centerX + smallerDimen / 17, centerY + smallerDimen / 10);
ctx.lineTo(centerX - smallerDimen / 9, centerY);
ctx.fillStyle = lb;
ctx.fill();
ctx.closePath();
screenshot();
ctx.beginPath();
ctx.strokeStyle = "rgb(255, 255, 255)";
ctx.moveTo(width - 20, 0);
ctx.lineTo(20, height);
ctx.stroke();
ctx.closePath();
}
function screenshot() {
canvasData = ctx.getImageData(0, 0, width, height).data;
}
function init() {
sizeCanvas();
drawNormalState();
}
init();
body {
margin: 0;
}
<canvas></canvas>
TL;DR the demo.
The best way I've found to do this is to 1) calculate "end points" for the line at the edge of (or outside) the canvas' bounds, 2) create two* polygons using the end points of the line generated in step 1 and the canvas' four corners, and 3) divide up the original canvas' image data into two new canvases based on the polygons we create.
* We actually create one, but the "second" is the remaining part of the original canvas.
1) Calculate the end points
You can use a very cheap algorithm to calculate some end points given a start coordinate, x and y difference (i.e. slope), and the bounds for the canvas. I used the following:
function getEndPoints(startX, startY, xDiff, yDiff, maxX, maxY) {
let currX = startX,
currY = startY;
while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
currX += xDiff;
currY += yDiff;
}
let points = {
firstPoint: [currX, currY]
};
currX = startX;
currY = startY;
while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
currX -= xDiff;
currY -= yDiff;
}
points.secondPoint = [currX, currY];
return points;
}
where
let xDiff = firstPoint.x - secondPoint.x,
yDiff = firstPoint.y - secondPoint.y;
2) Create two polygons
To create the polygons, I make use of Paul Bourke's Javascript line intersection:
function intersect(point1, point2, point3, point4) {
let x1 = point1[0],
y1 = point1[1],
x2 = point2[0],
y2 = point2[1],
x3 = point3[0],
y3 = point3[1],
x4 = point4[0],
y4 = point4[1];
// Check if none of the lines are of length 0
if((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return false;
}
let denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// Lines are parallel
if(denominator === 0) {
return false;;
}
let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
// is the intersection along the segments
if(ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return false;
}
// Return a object with the x and y coordinates of the intersection
let x = x1 + ua * (x2 - x1);
let y = y1 + ua * (y2 - y1);
return [x, y];
}
Along with some of my own logic:
let origin = [0, 0],
xBound = [width, 0],
xyBound = [width, height],
yBound = [0, height];
let polygon = [origin];
// Work clockwise from 0,0, adding points to our polygon as appropriate
// Check intersect with top bound
let topIntersect = intersect(origin, xBound, points.firstPoint, points.secondPoint);
if(topIntersect) {
polygon.push(topIntersect);
}
if(!topIntersect) {
polygon.push(xBound);
}
// Check intersect with right
let rightIntersect = intersect(xBound, xyBound, points.firstPoint, points.secondPoint);
if(rightIntersect) {
polygon.push(rightIntersect);
}
if((!topIntersect && !rightIntersect)
|| (topIntersect && rightIntersect)) {
polygon.push(xyBound);
}
// Check intersect with bottom
let bottomIntersect = intersect(xyBound, yBound, points.firstPoint, points.secondPoint);
if(bottomIntersect) {
polygon.push(bottomIntersect);
}
if((topIntersect && bottomIntersect)
|| (topIntersect && rightIntersect)) {
polygon.push(yBound);
}
// Check intersect with left
let leftIntersect = intersect(yBound, origin, points.firstPoint, points.secondPoint);
if(leftIntersect) {
polygon.push(leftIntersect);
}
3) Divide up the original canvas' image data
Now that we have our polygon, all that's left is putting this data into new canvases. The easiest way to do this is to use canvas' ctx.drawImage and ctx.globalCompositeOperation.
// Use or create 2 new canvases with the split original canvas
let newCanvas1 = document.querySelector("#newCanvas1");
if(newCanvas1 == null) {
newCanvas1 = document.createElement("canvas");
newCanvas1.id = "newCanvas1";
newCanvas1.width = width;
newCanvas1.height = height;
document.body.appendChild(newCanvas1);
}
let newCtx1 = newCanvas1.getContext("2d");
newCtx1.globalCompositeOperation = 'source-over';
newCtx1.drawImage(canvas, 0, 0);
newCtx1.globalCompositeOperation = 'destination-in';
newCtx1.beginPath();
newCtx1.moveTo(polygon[0][0], polygon[0][1]);
for(let item = 1; item < polygon.length; item++) {
newCtx1.lineTo(polygon[item][0], polygon[item][1]);
}
newCtx1.closePath();
newCtx1.fill();
let newCanvas2 = document.querySelector("#newCanvas2");
if(newCanvas2 == null) {
newCanvas2 = document.createElement("canvas");
newCanvas2.id = "newCanvas2";
newCanvas2.width = width;
newCanvas2.height = height;
document.body.appendChild(newCanvas2);
}
let newCtx2 = newCanvas2.getContext("2d");
newCtx2.globalCompositeOperation = 'source-over';
newCtx2.drawImage(canvas, 0, 0);
newCtx2.globalCompositeOperation = 'destination-out';
newCtx2.beginPath();
newCtx2.moveTo(polygon[0][0], polygon[0][1]);
for(let item = 1; item < polygon.length; item++) {
newCtx2.lineTo(polygon[item][0], polygon[item][1]);
}
newCtx2.closePath();
newCtx2.fill();
All of that put together gives us this demo!

How to draw only visible part of the tilemap on js canvas?

I created simple tilemap using Tiled (3200 x 3200 pixels). I loaded it on my canvas using this library
I draw entire tilemap 3200 x 3200 60 times per seocnd.
I tried to move around and it works fine. Btw, I move around canvas using ctx.translate. I included this in my own function
But when I created bigger map in Tiled ( 32000 x 32000 pixels ) - I got a very freezing page. I couldn't move around fast, I think there was about 10 fps
So how to fix it? I have to call drawTiles() function 60 times per second. But is there any way to draw only visible part of the tile? Like draw only what I see on my screen (0, 0, monitorWidth, monitorHeight I guess)
Thank you
##Drawing a large tileset
If you have a large tile set and only see part of it in the canvas you just need to calculate the tile at the top left of the canvas and the number of tiles across and down that will fit the canvas.
Then draw the square array of tiles that fit the canvas.
In the example the tile set is 1024 by 1024 tiles (worldTileCount = 1024), each tile is 64 by 64 pixels tileSize = 64, making the total playfield 65536 pixels square
The position of the top left tile is set by the variables worldX, worldY
###Function to draw tiles
// val | 0 is the same as Math.floor(val)
var worldX = 512 * tileSize; // pixel position of playfield
var worldY = 512 * tileSize;
function drawWorld(){
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
// get the tile position
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
// get the number of tiles that will fit the canvas
const tW = (canvas.width / s | 0) + 2;
const tH = (canvas.height / s | 0) + 2;
// set the location. Must floor to pixel boundary or you get holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
// Draw the tiles across and down
for(var y = 0; y < tH; y += 1){
for(var x = 0; x < tW; x += 1){
// get the index into the tile array for the tile at x,y plus the topleft tile
const i = tx + x + (ty + y) * c;
// get the tile id from the tileMap. If outside map default to tile 6
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
// draw the tile at its location. last 2 args are x,y pixel location
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
###setTransform and absolute coordinates.
Use absolute coordinates makes everything simple.
Use the canvas context setTransform to set the world position and then each tile can be drawn at its own coordinate.
// set the world location. The | 0 floors the values and ensures no holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
That way if you have a character at position 51023, 34256 you can just draw it at that location.
playerX = 51023;
playerY = 34256;
ctx.drawImage(myPlayerImage,playerX,playerY);
If you want the tile map relative to the player then just set the world position to be half the canvas size up and to the left plus one tile to ensure overlap
playerX = 51023;
playerY = 34256;
worldX = playerX - canvas.width / 2 - tileWidth;
worldY = playerY - canvas.height / 2 - tileHeight;
###Demo of large 65536 by 65536 pixel tile map.
At 60fps if you have the horses and can handle much much bigger without any frame rate loss. (map size limit using this method is approx 4,000,000,000 by 4,000,000,000pixels (32 bit integers coordinates))
#UPDATE 15/5/2019 re Jitter
The comments have pointed out that there is some jitter as the map scrolls.
I have made changes to smooth out the random path with a strong ease in out turn every 240 frame (4 seconds at 60fps) Also added a frame rate reducer, if you click and hold the mouse button on the canvas the frame rate will be slowed to 1/8th normal so that the jitter is easier to see.
There are two reasons for the jitter.
###Time error
The first and least is the time passed to the update function by requestAnimationFrame, the interval is not perfect and rounding errors due to the time is compounding the alignment problems.
To reduce the time error I have set the move speed to a constant interval to minimize the rounding error drift between frames.
###Aligning tiles to pixels
The main reason for the jitter is that the tiles must be rendered on pixel boundaries. If not then aliasing errors will create visible seams between tiles.
To see the difference click the button top left to toggle pixel alignment on and off.
To get smooth scrolling (sub pixel positioning) draw the map to an offscreen canvas aligning to the pixels, then render that canvas to the display canvas adding the sub pixel offset. That will give the best possible result using the canvas. For better you will need to use webGL
###End of update
var refereshSkip = false; // when true drops frame rate by 4
var dontAlignToPixel = false;
var ctx = canvas.getContext("2d");
function mouseEvent(e) {
if(e.type === "click") {
dontAlignToPixel = !dontAlignToPixel;
pixAlignInfo.textContent = dontAlignToPixel ? "Pixel Align is OFF" : "Pixel Align is ON";
} else {
refereshSkip = e.type === "mousedown";
}
}
pixAlignInfo.addEventListener("click",mouseEvent);
canvas.addEventListener("mousedown",mouseEvent);
canvas.addEventListener("mouseup",mouseEvent);
// wait for code under this to setup
setTimeout(() => {
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// create tile map
const worldTileCount = 1024;
const tileMap = new Uint8Array(worldTileCount * worldTileCount);
// add random tiles
doFor(worldTileCount * worldTileCount, i => {
tileMap[i] = randI(1, tileCount);
});
// this is the movement direction of the map
var worldDir = Math.PI / 4;
/* =======================================================================
Drawing the tileMap
========================================================================*/
var worldX = 512 * tileSize;
var worldY = 512 * tileSize;
function drawWorld() {
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
const tW = (canvas.width / s | 0) + 2; // get the number of tiles to fit canvas
const tH = (canvas.height / s | 0) + 2;
// set the location
if(dontAlignToPixel) {
ctx.setTransform(1, 0, 0, 1, -worldX,-worldY);
} else {
ctx.setTransform(1, 0, 0, 1, Math.floor(-worldX),Math.floor(-worldY));
}
// Draw the tiles
for (var y = 0; y < tH; y += 1) {
for (var x = 0; x < tW; x += 1) {
const i = tx + x + (ty + y) * c;
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
var timer = 0;
var refreshFrames = 0;
const dirChangeMax = 3.5;
const framesBetweenDirChange = 240;
var dirChangeDelay = 1;
var dirChange = 0;
var prevDir = worldDir;
const eCurve = (v, p = 2) => v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p);
//==============================================================
// main render function
function update() {
refreshFrames ++;
if(!refereshSkip || (refereshSkip && refreshFrames % 8 === 0)){
timer += 1000 / 60;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
// Move the map
var speed = Math.sin(timer / 10000) * 8;
worldX += Math.cos(worldDir) * speed;
worldY += Math.sin(worldDir) * speed;
if(dirChangeDelay-- <= 0) {
dirChangeDelay = framesBetweenDirChange;
prevDir = worldDir = prevDir + dirChange;
dirChange = rand(-dirChangeMax , dirChangeMax);
}
worldDir = prevDir + (1-eCurve(dirChangeDelay / framesBetweenDirChange,3)) * dirChange;
// Draw the map
drawWorld();
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}, 0);
/*===========================================================================
CODE FROM HERE DOWN UNRELATED TO THE ANSWER
===========================================================================*/
const imageTools = (function() {
// This interface is as is. No warenties no garenties, and NOT to be used comercialy
var workImg, workImg1, keep; // for internal use
keep = false;
var tools = {
canvas(width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage: function(width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
drawSpriteQuick: function(image, spriteIndex, x, y) {
var w, h, spr;
spr = image.sprites[spriteIndex];
w = spr.w;
h = spr.h;
ctx.drawImage(image, spr.x, spr.y, w, h, x, y, w, h);
},
line(x1, y1, x2, y2) {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
},
circle(x, y, r) {
ctx.moveTo(x + r, y);
ctx.arc(x, y, r, 0, Math.PI * 2);
},
};
return tools;
})();
const doFor = (count, cb) => {
var i = 0;
while (i < count && cb(i++) !== true);
}; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const seededRandom = (() => {
var seed = 1;
return {
max: 2576436549074795,
reseed(s) {
seed = s
},
random() {
return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
}
}
})();
const randSeed = (seed) => seededRandom.reseed(seed | 0);
const randSI = (min, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const tileSize = 64;
const tileCount = 7;
function drawGrass(ctx, c1, c2, c3) {
const s = tileSize;
const gs = s / (8 * c3);
ctx.fillStyle = c1;
ctx.fillRect(0, 0, s, s);
ctx.strokeStyle = c2;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.beginPath();
doFor(s, i => {
const x = rand(-gs, s + gs);
const y = rand(-gs, s + gs);
const x1 = rand(x - gs, x + gs);
const y1 = rand(y - gs, y + gs);
imageTools.line(x, y, x1, y1);
imageTools.line(x + s, y, x1 + s, y1);
imageTools.line(x - s, y, x1 - s, y1);
imageTools.line(x, y + s, x1, y1 + s);
imageTools.line(x, y - s, x1, y1 - s);
})
ctx.stroke();
}
function drawTree(ctx, c1, c2, c3) {
const seed = Date.now();
const s = tileSize;
const gs = s / 2;
const gh = gs / 2;
ctx.fillStyle = c1;
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 8;
ctx.shadowOffsetY = 8;
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss), randS(gs - gh * ss, gs + gh * ss), randS(gh / 4, gh / 2));
})
ctx.stroke();
ctx.fill();
ctx.restore();
ctx.fillStyle = c2;
ctx.strokeStyle = c3;
ctx.lineWidth = 2;
ctx.save();
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss) - 2, randS(gs - gh * ss, gs + gh * ss) - 2, randS(gh / 4, gh / 2) / 1.6);
})
ctx.stroke();
ctx.fill();
ctx.restore();
}
const tileRenders = [
(ctx) => {
drawGrass(ctx, "#4C4", "#4F4", 1)
},
(ctx) => {
drawGrass(ctx, "#644", "#844", 2)
},
(ctx) => {
tileRenders[0](ctx);
drawTree(ctx, "#480", "#8E0", "#7C0")
},
(ctx) => {
tileRenders[1](ctx);
drawTree(ctx, "#680", "#AE0", "#8C0")
},
(ctx) => {
drawGrass(ctx, "#008", "#00A", 4)
},
(ctx) => {
drawGrass(ctx, "#009", "#00C", 4)
},
(ctx) => {
drawGrass(ctx, "#00B", "#00D", 4)
},
]
const tileSet = imageTools.createImage(tileSize * tileCount, tileSize);
const ctxMain = ctx;
ctx = tileSet.ctx;
tileSet.sprites = [];
doFor(tileCount, i => {
x = i * tileSize;
ctx.save();
ctx.setTransform(1, 0, 0, 1, x, 0);
ctx.beginPath();
ctx.rect(0, 0, tileSize, tileSize);
ctx.clip()
if (tileRenders[i]) {
tileRenders[i](ctx)
}
tileSet.sprites.push({
x,
y: 0,
w: tileSize,
h: tileSize
});
ctx.restore();
});
ctx = ctxMain;
canvas {
position: absolute;
top: 0px;
left: 0px;
}
div {
position: absolute;
top: 8px;
left: 8px;
color: white;
}
#pixAlignInfo {
color: yellow;
cursor: pointer;
border: 2px solid green;
margin: 4px;
}
#pixAlignInfo:hover {
color: white;
background: #0008;
cursor: pointer;
}
body {
background: #49c;
}
<canvas id="canvas"></canvas>
<div>Hold left button to slow to 1/8th<br>
<span id="pixAlignInfo">Click this button to toggle pixel alignment. Alignment is ON</span></div>

Better line selection than using Bresenham algorithm?

I'm drawing lines on an HTML canvas, and use a less precise 2d-array (representing blocks of 10x10 pixels) in which I 'draw' lines with Bresenham's algorithm to store line-ids, so I can use that array to see which line is selected.
This works, but I would like it to be more accurate - not in the 10x10 size that I use (I like that I don't exactly have to click on the line), but when I draw a representation of that array over my actual canvas, I see that there are a lot of the 10x10 blocks not filled, even though the line is crossing them:
Is there a better solution to this? What I want is to catch ALL grid blocks that the actual line passes through.
Without seeing your code, I think you made a rounding error while filling the lookup table using the Bresenham algorithm or you scaled the coordinates before running the algorithm.
This jsFiddle shows what I came up with and the squares are perfectly aligned.
HTML
<canvas id="myCanvas"></canvas>
CSS
#myCanvas {
width: 250px;
height: 250px;
}
JavaScript
var $canvas = $("#myCanvas"),
ctx = $canvas[0].getContext("2d");
ctx.canvas.width = $canvas.width();
ctx.canvas.height = $canvas.height();
function Grid(ctx) {
this._ctx = ctx;
this._lines = [];
this._table = [];
this._tableScale = 10;
this._createLookupTable();
}
Grid.prototype._createLookupTable = function() {
this._table = [];
for (var y = 0; y < Math.ceil(ctx.canvas.height / this._tableScale); y++) {
this._table[y] = [];
for (var x = 0; x < Math.ceil(ctx.canvas.width / this._tableScale); x++)
this._table[y][x] = null;
}
};
Grid.prototype._updateLookupTable = function(line) {
var x0 = line.from[0],
y0 = line.from[1],
x1 = line.to[0],
y1 = line.to[1],
dx = Math.abs(x1 - x0),
dy = Math.abs(y1 - y0),
sx = (x0 < x1) ? 1 : -1,
sy = (y0 < y1) ? 1 : -1,
err = dx - dy;
while(true) {
this._table[Math.floor(y0 / 10)][Math.floor(x0 / 10)] = line;
if ((x0 == x1) && (y0 == y1)) break;
var e2 = 2 * err;
if (e2 >- dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
};
Grid.prototype.hitTest = function(x, y) {
var ctx = this._ctx,
hoverLine = this._table[Math.floor(y / 10)][Math.floor(x / 10)];
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
this._lines.forEach(function(line) {
line.draw(ctx, line === hoverLine ? "red" : "black");
});
};
Grid.prototype.drawLookupTable = function() {
ctx.beginPath();
for (var y = 0; y < this._table.length; y++)
for (var x = 0; x < this._table[y].length; x++) {
if (this._table[y][x])
ctx.rect(x * 10, y * 10, 10, 10);
}
ctx.strokeStyle = "rgba(0, 0, 0, 0.2)";
ctx.stroke();
};
Grid.prototype.addLine = function(line) {
this._lines.push(line);
this._updateLookupTable(line);
};
Grid.prototype.draw = function() {
var ctx = this._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
this._lines.forEach(function(line) {
line.draw(ctx);
});
};
function Line(x0, y0, x1, y1) {
this.from = [ x0, y0 ];
this.to = [ x1, y1];
}
Line.prototype.draw = function(ctx, style) {
ctx.beginPath();
ctx.moveTo(this.from[0], this.from[1]);
ctx.lineTo(this.to[0], this.to[1]);
ctx.strokeStyle = style || "black";
ctx.stroke();
};
var grid = new Grid(ctx);
grid.addLine(new Line(80, 10, 240, 75));
grid.addLine(new Line(150, 200, 50, 45));
grid.addLine(new Line(240, 10, 20, 150));
grid.draw();
grid.drawLookupTable();
$canvas.on("mousemove", function(e) {
grid.hitTest(e.offsetX, e.offsetY);
grid.drawLookupTable();
});
Your best option is to treat the mouse-cursor-position as a small circle (f.e. with a 5px radius) and check if the line intersects with the circle.
Use the math as explained in this Q&A
JavaScript
A simple function to detect intersection would be:
function lineCircleIntersects(x1, y1, x2, y2, cx, cy, cr) {
var dx = x2 - x1,
dy = y2 - y1,
a = dx * dx + dy * dy,
b = 2 * (dx * (x1 - cx) + dy * (y1 - cy)),
c = cx * cx + cy * cy,
bb4ac;
c += x1 * x1 + y1 * y1;
c -= 2 * (cx * x1 + cy * y1);
c -= cr * cr;
bb4ac = b * b - 4 * a * c;
return bb4ac >= 0; // true: collision, false: no collision
}
See it working in this jsFiddle, but note that this function will also return true if the cursor is on the slope of the line outside [x1, y1], [x2, y2]. I'll leave this up to you :)
You can also try line-circle-collision library on github which should give you what you want.

Categories