JavaScript - Calculate area of region drawn on canvas - javascript

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

Related

How to programmatically draw a thick line JavaScript?

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

Executing function once in a game loop

I have a game loop that refreshes ~20 times per second with the following condition, inside function growPlayerBlockCounterWinner():
if (obstacleArray[i][2] > canvas.height) {player.size += 5;}
That means: if black object goes outside the canvas area, player size will increase by 5 pixels.
The function normally executes once, but due to putting it in a game loop, it executes each time the game is refreshed, so player is growing continuously.
Previously i had it with the sign '=' instead od '>', but that was working well when black blocks were moving pixel by pixel and i want them to move faster.
You can inspect the problem and full code on the remote server: https://stacho163.000webhostapp.com/firstLevel.html
Below I paste only my obstacle functions:
// js game script //
let obstacle = {
size: 0,
posX: 0,
posY: 0,
speed: 0
}
let obstacleArray = new Array(100);
function generateObstacle() {
for (i = 0; i < obstacleArray.length; i++) {
obstacle.size = Math.round((Math.random() * 100) + 50);
obstacle.posX = Math.round((Math.random() * (canvas.width -
obstacle.size)));
obstacle.posY = -450 - (i * 100);
obstacle.speed = 5;
obstacleArray[i] = [obstacle.size, obstacle.posX, obstacle.posY,
obstacle.speed];
}
}
function drawObstacle() {
for (i = 0; i < obstacleArray.length; i++) {
ctx.fillStyle = "#000";
ctx.fillRect(obstacleArray[i][1], obstacleArray[i][2],
obstacleArray[i][0], obstacleArray[i][0]);
obstacleArray[i][2] += obstacleArray[i][3];
}
}
function growPlayerBlockCounterWinner() {
for (i = 0; i < obstacleArray.length; i++) {
// grow player
if (obstacleArray[i][2] > canvas.height) {
player.size += 5;
}
}
}
generateObstacle();
function game() {
drawObstacle();
growPlayerBlockCounterWinner();
requestAnimationFrame(game);
}
requestAnimationFrame(game);
I am looking for opinions, maybe my logic about that statement is incorrect or i should place that statement in other place.
Thanks for your tips :)
As you have a defined number of black boxes, you might be able to store, for which black box the player already increased size like this:
let obstacle = {...};
var affectedBlackBoxes = {};
//...//
function growPlayerBlockCounterWinner() {
for (i = 0; i < obstacleArray.length; i++) {
// grow player
if (obstacleArray[i][2] > canvas.height && !affectedBlackBoxes[i]) {
player.size += 5;
affectedBlackBoxes[i] = true;
}
}
}

HTML Canvas putImageData causing frame overlap

I am trying to read pixels (frame by frame) from websocket and render it on to canvas (in grayscale). Data is being displayed properly, just that pixels of the current frame are displayed overlapped on previous frame. Thus picture gets smudged after few frames.
I am tying to clear out the previous frame before rendering current one. What would be the proper way to do it?
I am using putImageData() to render the frame. I have tried clearRect() before calling putImageData(). and have tried clearing Imgdata (Imgdata.data = [];) array before populating it again but none of these things worked.
` var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var Imgdata = context.createImageData(100,100);
var numPixel = Imgdata.data.length/4;
ws.onmessage = function (event) {
var bytes = new Uint8Array(event.data);
console.log("data: " + numPixel + " bytes: "+ bytes.length);
//Imgdata.data = [];
for (var i = 0; i < numPixel; i++) {
Imgdata.data[(i*4)+Math.round(bytes[i]/85)] = (bytes[i]%85)*3;
Imgdata.data[(i*4)+3] = 255;
Imgdata.data[(i*4)+0] = bytes[i];
Imgdata.data[(i*4)+1] = bytes[i];
Imgdata.data[(i*4)+2] = bytes[i];
Imgdata.data[(i*4)+3] = 255;
}
//context.clearRect(0, 0, canvas.width, canvas.height);
context.putImageData(Imgdata,0,0);
};
`
I would say the cause is most likely due to the first line inside the for loop, without knowing the data i dont know what it does but id does not look right.
// ???
Imgdata.data[(i*4)+Math.round(bytes[i]/85)] = (bytes[i]%85)*3;
If the data is random then this would randomly set one of the colour channels of each pixel.
Apart from that I can not see any errors that would cause what you describe.
I have rewritten the code only as a suggestion to improve the performance a little.
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const imgdata = context.createImageData(100,100);
const pix = imgdata.data;
pix.fill(255); // set all to 255 To cover the alpha channel
const numPixel = pix.length; // best leave as byte count
ws.onmessage = function (event) {
const inData = event.data;
var inIndex = 0;
var i = 0;
while(i < numPixel){
pix[i ++] = pix[i ++] = pix[i ++] = inData[inIndex ++];
i++;
}
context.putImageData(imgdata,0,0);
};
That should work event if the incoming data is incomplete the function putImageData completely replaces the pixels. If any pixels are undefined from the socket data they will be set to zero and will show up as black.

getImageData memory leak?

Background: Over the last week I've been working on a game that is essentially multi-directional Tron, using Canvas and JavaScript. I opted not to clear the Canvas every frame so that my little line segments leave a trail. For collision detection, I use this function:
// 8 sensors for collision testing, positioned evenly around the brush point
var detectionRadius = this.width / 2 + 1; //points just outside the circumference
var counter = 0;
var pixelData;
for (var i = 0; i < 16; i += 2) {
//collisionPixels[] is an array of 8 (x, y) offsets, spaced evenly around the center of the circle
var x = this.x + collisionPixels[i] * detectionRadius;
var y = this.y + collisionPixels[i + 1] * detectionRadius;
pixelData = context.getImageData(x,y,1,1).data; //pixel data at each point
if (pixelData[3] != 0) {
counter++;
}
}
if (counter > 4) {
this.collision();
}
The purpose here is to get the alpha values of 8 pixels around the brushpoint's surface; alpha values of 0 are just on the background. If the number of colliding pixels, out of the total 8, is greater than 4 (this is including the trail behind the player) then I call the collision() method. This function actually works really well (and this IS inside a function, so these declarations are local).
The problem is that context.getImageData() skyrockets my memory usage, and after 3 or 4 games tanks the framerate. Cutting just that line out and assigning pixelData some other value makes everything run very smoothly, even while doing the other computations.
How do I fix this memory leak? And, if there's a less convoluted way to do collision detection of this type, what is it?
EDIT: at request, here is my loop:
function loop() {
now = Date.now();
delta = now - lastUpdate;
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
}
requestAnimationFrame(loop);
}
EDIT 2: So I tried Patrick's UInt8ClampedArrays idea:
//8 sensors for collision testing, positioned evenly around the brush point
var detectionRadius = this.width / 2 + 1;
var counter = 0;
for (var i = 0; i < 16; i += 2) {
var x = this.x + collisionPixels[i] * detectionRadius;
var y = this.y + collisionPixels[i + 1] * detectionRadius;
//translate into UInt8ClampedArray for data
var index = (y * canvas.width + x) * 4 + 3; //+3 so we're at the alpha index
if (canvasArray[index] != 0) {
counter++;
}
}
And, at the top of my loop I added a new global variable, updated once per frame:
var canvasArray = context.getImageData(0,0,canvas.width,canvas.height).data;
Hope I did that right. It works, but the memory and framerate still get worse each round you play. Going to upload some heap snapshots.
EDIT 3:
Snapshot 1: https://drive.google.com/open?id=0B-8p3yyYzRjeY2pEa2Z5QlgxRUk&authuser=0
Snapshot 2: https://drive.google.com/open?id=0B-8p3yyYzRjeV2pJb1NyazY3OWc&authuser=0
Snapshot 1 is after the first game, 2 is after the second.
EDIT 4: Tried capping the framerate:
function loop() {
requestAnimationFrame(loop);
now = Date.now();
delta = now - lastUpdate;
//lastUpdate = now;
if (delta > interval) {
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
}
}
}
Where
interval = 1000 / fps;
It delays the eventual performance hit, but memory is still climbing with this option.
EDIT 5: While I'm sure there must be a better way, I found a solution that works reasonably well. Capping the framerate around 30 actually worked in terms of long-term performance, but I hated the way the game looked at 30 FPS.. so I built a loop that had an uncapped framerate for all updating and rendering EXCEPT for collision handling, which I updated at 30 FPS.
function loop() {
requestAnimationFrame(loop);
now = Date.now();
delta = now - lastUpdate;
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
if (now - lastCollisionUpdate > collisionInterval) {
canvasData = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
if (players[i].detectCollisions()) {
players[i].collision();
}
}
}
lastCollisionUpdate = now;
}
canvasData = null;
}
}
Thanks for the answers.. a lot of your ideas found their way into the final(?) product, and I appreciate that. Closing this thread.
Is there some point at which you could call context.getImageData(0, 0, context.canvas.width, context.canvas.height).data so that you can use that single UInt8ClampedArray instead of however many you're using? Also when you're done with the image data (the ImageData that is, not the TypedArray inside it), you could try calling delete on it, though I'm not certain if that will deallocate the memory.
While I'm sure there must be a better way, I found a solution that works reasonably well. Capping the framerate around 30 actually worked in terms of long-term performance, but I hated the way the game looked at 30 FPS.. so I built a loop that had an uncapped framerate for all updating and rendering EXCEPT for collision handling, which I updated at 30 FPS.
//separate update cycle for collision detection
var collisionFPS = 30;
var lastCollisionUpdate;
var collisionInterval = 1000 / collisionFPS;
var canvasData;
function loop() {
requestAnimationFrame(loop);
now = Date.now();
delta = now - lastUpdate;
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
if (now - lastCollisionUpdate > collisionInterval) {
canvasData = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
if (players[i].detectCollisions()) {
players[i].collision();
}
}
}
lastCollisionUpdate = now;
}
canvasData = null;
}
}
Might not be the best solution, but it's consistent.

HTML5 Canvas game development complication

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

Categories

Resources