Minimizing canvas "bitmap" data size - javascript
Context: multi-user app (node.js) - 1 painter, n clients
Canvas size: 650x400 px (= 260,000 px)
For the canvas to be updated frequently (I'm thinking about 10 times a second), I need to keep the data size as small as possible, especially when thinking about upload rates.
The toDataURL() method returning a base64 string is fine but it contains masses of data I don't even need (23 bit per pixel). Its length is 8,088 (without the preceding MIME information), and assuming the JavaScript strings have 8-bit encoding that would be 8.1 kilobytes of data, 10 times per second.
My next try was using JS objects for the different context actions like moveTo(x, y) or lineTo(x, y), sending them to the server and have the clients receive the data in delta updates (via timestamps). However, this turned out to be even less efficient than the base64 string.
{
"timestamp": 0,
"what": {
"name": LINE_TO,
"args": {"x": x, "y": y}
}
}
It doesn't work fluently nor precisely because there are nearly 300 lineTo commands already when you swipe your brush shortly. Sometimes there's a part of the movement missing (making a line straight instead of rounded), sometimes the events aren't even recognized by the script client-side because it seems to be "overwhelmed" by the mass of events triggered already.
So I have to end up using the base64 string with its 8.1 KB. I don't want to worry about this much - but even if done asynchronously with delta updates, there will be major lags on a real server, let alone the occasional bandwidth overrun.
The only colors I am using are #000 and #FFF, so I was thinking about a 1-bit data structure with delta updates only. This would basically suffice and I wouldn't mind any "color" precision losses (it is black after all).
With most of the canvas being white, you could think of additional Huffman run-length encoding to reduce size even further, too. Like a canvas with a size of 50x2 px and a single black pixel at (26, 2) would return the following string: 75W1B74W (50 + 25 white pixels, then 1 black pixel, then 24 more white pixels)
It would even help if the canvas consisted of a 1-bit string like this:
00000000000000000000000000000000000000000000000000
00000000000000000000000001000000000000000000000000
That would help a lot already.
My first question is: How to write an algorithm to get this data efficiently?
The second is: How could I pass the pure binary canvas data to the clients (via node server)? How do I even send a 1-bit data structure to the server? Would I have to convert my bits to a hexadecimal (or more) number and re-parse?
Would it be possible to use this as a data structure?
Thanks in advance,
Harti
I need to keep the data size as small as possible
Then don't send the entire data. Send only the changes, close to what you propose yourself.
Make the framework such that every user can only do "actions" such as "draw black strokeWidth 2 from X1,Y1 to X2,Y2".
I wouldn't bother with some pure binary thing. If there's only two colors then that's easy to send as the string "1,2,x,y,x2,y2", which the other people will parse precisely the same way the local client will, and it will get drawn the same way.
I wouldn't overthink this. Get it working with simple strings before you worry about any clever encoding. It's worth trying the simple thing first. Maybe the performance will be quite good without going through a lot of trouble!
I sorted it out, finally. I used an algorithm to get the image data within a specified area (i.e. the area currently drawn on), and then paste the image data to the same coordinates.
While drawing, I keep my application informed about how big the modified area is and where it starts (stored in currentDrawingCoords).
pixels is an ImageData Array obtained by calling context.getImageData(left, top, width, height) with the stored drawing coordinates.
getDeltaUpdate is called upon onmouseup (yeah, that's the drawback of the area idea):
getDeltaUpdate = function(pixels, currentDrawingCoords) {
var image = "" +
currentDrawingCoords.left + "," + // x
currentDrawingCoords.top + "," + // y
(currentDrawingCoords.right - currentDrawingCoords.left) + "," + // width
(currentDrawingCoords.bottom - currentDrawingCoords.top) + ""; // height
var blk = 0, wht = 0, d = "|";
// http://stackoverflow.com/questions/667045/getpixel-from-html-canvas
for (var i=0, n=pixels.length; i < n; i += 4) {
if(
pixels[i] > 0 ||
pixels[i+1] > 0 ||
pixels[i+2] > 0 ||
pixels[i+3] > 0
) {
// pixel is black
if(wht > 0 || (i == 0 && wht == 0)) {
image = image + d + wht;
wht = 0;
d = ",";
}
blk++;
//console.log("Pixel " + i + " is BLACK (" + blk + "-th in a row)");
} else {
// pixel is white
if(blk > 0) {
image = image + d + blk;
blk = 0;
d = ",";
}
wht++;
//console.log("Pixel " + i + " is WHITE (" + blk + "-th in a row)");
}
}
return image;
}
image is a string with a header part (x,y,width,height|...) and a data body part (...|w,b,w,b,w,[...])
The result is a string with less characters than the base64 string has (as opposed to the 8k characters string, the delta updates have 1k-6k characters, depending on how many things have been drawn into the modification area)
That string is sent to the server, pushed to all the other clients and reverted to ImageData by using getImageData:
getImageData = function(imagestring) {
var data = imagestring.split("|");
var header = data[0].split(",");
var body = data[1].split(",");
var where = {"x": header[0], "y": header[1]};
var image = context.createImageData(header[2], header[3]); // create ImageData object (width, height)
var currentpixel = 0,
pos = 0,
until = 0,
alpha = 0,
white = true;
for(var i=0, n=body.length; i < n; i++) {
var pixelamount = parseInt(body[i]); // amount of pixels with the same color in a row
if(pixelamount > 0) {
pos = (currentpixel * 4);
until = pos + (pixelamount * 4); // exclude
if(white) alpha = 0;
else alpha = 255;
while(pos < until) {
image.data[pos] = 0;
image.data[pos+1] = 0;
image.data[pos+2] = 0;
image.data[pos+3] = alpha;
pos += 4;
}
currentpixel += pixelamount;
white = (white ? false : true);
} else {
white = false;
}
}
return {"image": image, "where": where};
}
Call context.putImageData(data.image, data.where.x, data.where.y); to put the area on top of everything there is!
As previously mentioned, this may not be the perfect suit for every kind of monochrome canvas drawing application since the modification area is only submit onmouseup. However, I can live with this trade-off because it's far less stressful for the server than all the other methods presented in the question.
I hope I was able to help the people to follow this question.
Related
Isolating a part of a string
well... for begining, i m building some dumb and easy projects because i'm starting learning the world of front end development. One of those you can find in internet was this Color picker idea. Some page where with a button you can generate a random color. all random, no connections between the colors o something else. (i'm not a native english speaker so sorry if i write something and you dont get it right) Here's the link to the repository so... I build a function that makes a random HSL color randomizing the values and then building a string and puting that in the css function getRandomHsl(){ let hueValue = 0; hueValue = Math.floor( Math.random() * (360 + 1) ) let satValue = 0; satValue = Math.floor( Math.random() * (100 + 1) ) let lightValue = 0; lightValue = Math.floor( Math.random() * (100 + 1) ) return 'hsl(' + hueValue + ', ' + satValue + '%, ' + lightValue + '%)';} if you can give me your opinion on that function and tell me if you would have done it inanother way. so... i wanted to find the way to if the Lightness would have a low value the color of the font change it to some white or something like that. And this is what i figured out. (the first part of the code is the button changing the value of the background color of the main div. This works well. The problem comes after). hslBtn.addEventListener('click', () => { let hslStringComplete = getRandomHsl(); colorShowcase.textContent = hslStringComplete; document.getElementById('color-container').style.backgroundColor = hslStringComplete; /*================================= change the color font od the text when its to dark */ let hslLightValue = hslStringComplete; let lightValue = hslLightValue.toString().substr(13,3).replace(/\D/g,'') console.log(hslLightValue.substr(13,3).replace(/\D/g,'')); if(lightValue < 40){ innerHTML('input').style.color = "white"; }}) i have really strougle it out coming up with this idea so i want some opinion on how could i have done in other way. PD: `if(lightValue < 40){ innerHTML('input').style.color = "white";} this is hte HTML: <input class="background-container"><h3 class="forWhiteColor">Background Color : <span class="color-showcase">#messi</span></h3></input> this part get me an error and the html doesn get changes. Here's the error. console error thanks in advance.
innerHTML('input') should probably be document.getElementById('color-container') instead. also you should note that when you extract a part of a string, you get a string. And comparing a string to a number won't work well, you first need to convert your string to a number with parseInt (for integers) or parseFloat (for float)
Is it possible to draw function like tangens wihout knowing its domain? [duplicate]
I am making a graphing program in C++ using the SFML library. So far I have been able to draw a function to the screen. I have run into two problems along the way. The first is a line which seems to return to the origin of my the plane, starting from the end of my function. You can see it in this image: As you can see this "rogue" line seems to change colour as it nears the origin. My first question is what is this line and how may I eradicate it from my window? The second problem which is slightly unrelated and more mathematical can be seen in this image: As you can see the asymptotes which are points where the graph is undefined or non continuous are being drawn. This leads me to my second question: is there a way ( in code ) to identify an asymptote and not draw it to the window. My code for anything drawn to the window is: VertexArray axis(Lines, 4); VertexArray curve(PrimitiveType::LinesStrip, 1000); axis[0].position = Vector2f(100000, 0); axis[1].position = Vector2f(-100000, 0); axis[2].position = Vector2f(0, -100000); axis[3].position = Vector2f(0, 100000); float x; for (x = -pi; x < pi; x += .0005f) { curve.append(Vertex(Vector2f(x, -tan(x)), Color::Green)); } I would very much appreciate any input : ) Update: Thanks to the input of numerous people this code seems to work fine in fixing the asymptote problem: for (x = -30*pi; x < 30*pi; x += .0005f) { x0 = x1; y0 = y1; x1 = x; y1 = -1/sin(x); a = 0; a = fabs(atan2(y1 - y0, x1 - x0)); if (a > .499f*pi) { curve.append(Vertex(Vector2f(x1, y1), Color::Transparent)); } else { curve.append(Vertex(Vector2f(x1, y1), Color::Green)); } } Update 2: The following code gets rid of the rogue line: VertexArray curve(Lines, 1000); float x,y; for (x = -30 * pi; x < 30 * pi; x += .0005f) { y = -asin(x); curve.append(Vertex(Vector2f(x, y))); } for (x = -30 * pi + .0005f; x < 30 * pi; x += .0005f) { y = -asin(x); curve.append(Vertex(Vector2f(x, y))); }
The first problem looks like a wrong polyline/curve handling. Don't know what API are you using for rendering but some like GDI need to start the pen position properly. For example if you draw like this: Canvas->LineTo(x[0],y[0]); Canvas->LineTo(x[1],y[1]); Canvas->LineTo(x[2],y[2]); Canvas->LineTo(x[3],y[3]); ... Then you should do this instead: Canvas->MoveTo(x[0],y[0]); Canvas->LineTo(x[1],y[1]); Canvas->LineTo(x[2],y[2]); Canvas->LineTo(x[3],y[3]); ... In case your API needs MoveTo command and you are not setting it then last position is used (or default (0,0)) which will connect start of your curve with straight line from last draw or default pen position. Second problem In continuous data you can threshold the asymptotes or discontinuities by checking the consequent y values. If your curve render looks like this: Canvas->MoveTo(x[0],y[0]); for (i=1;i<n;i++) Canvas->LineTo(x[i],y[i]); Then you can change it to something like this: y0=y[0]+2*threshold; for (i=0;i<n;i++) { if (y[1]-y0>=threshold) Canvas->MoveTo(x[i],y[i]); else Canvas->LineTo(x[i],y[i]); y0=y[i]; } The problem is selection of the threshold because it is dependent on x density of sampled points and on the first derivation of your y data by x (slope angles) If you are stacking up more functions the curve append will create your unwanted line ... instead handle each data as separate draw or put MoveTo command in between them [Edit1] I see it like this (fake split): double x0,y0,x1,y1,a; for (e=1,x = -pi; x < pi; x += .0005f) { // last point x0=x1; y0=y1; // your actual point x1=x; y1=-tan(x); // test discontinuity if (e) { a=0; e=0; } else a=fabs(atan2(y1-y0,x1-x0)); if (a>0.499*M_PI) curve.append(Vertex(Vector2f(x1,y1), Color::Black)); else curve.append(Vertex(Vector2f(x1,y1), Color::Green)); } the 0.499*M_PI is you threshold the more closer is to 0.5*M_PIthe bigger jumps it detects... I faked the curve split by black color (background) it will create gaps on axis intersections (unless transparency is used) ... but no need for list of curves ...
Those artifacts are due to the way sf::PrimitiveType::LinesStrip works (or more specific lines strips in general). In your second example, visualizing y = -tan(x), you're jumping from positive infinity to negative infinity, which is the line you're seeing. You can't get rid of this, unless you're using a different primitive type or splitting your rendering into multiple draw calls. Imagine a line strip as one long thread you're pinning with pushpins (representing your vertices). There's no (safe) way to go from positive infinity to negative infinity without those artifacts. Of course you could move outside the visible area, but then again that's really specific to this one function.
HTML Canvas game: 2D collision detection
I need to program a very simple 2D HTML canvas game with a character and a few walls. The map (top view) is a multidimensional array (1=walls) map = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0], [1,0,0,1,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1], [1,0,0,1,0,0,0,0,1,0,1,1,1,1,1,1,0,0,0,0,0,0,0,1], [1,0,0,1,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,1], [1,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], [1,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1], [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,1,1], [1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0,0,1,1,0,1,1], [1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,0,0,0,0,1,1,0,1,1], [0,0,0,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,1,1,1,0,1,1], [1,1,1,0,1,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,1,0,1,1], [1,1,1,0,0,0,0,0,0,0,0,1,1,0,0,1,1,1,0,0,0,0,1,1], [1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,1,0,0,1,1], [1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ]; The character shouldn't be able to walk over the walls... so he should only walk on the "0"s. I already got the map rendering and the walking part of the character working just fine, but I can't quite figure out how to check for collisions yet. A very simple version can be found on JSBin. You can either use the arrow keys or WASD to move around (black square). I already tried to do a very simple collision detection by using something like this: function checkCollision( x, y ) { if ( map[ Math.round( x ) ][ Math.round( y ) ] !== 0 ) { return true; // Collision } return false; } But this doesn't quite work (see JSBin). With Math.round the character and wall overlap... if I use Math.ceil or Math.floor it's even worse. Is there any way that I can improve this "collision detection", so that the character can't walk over the red walls?
There are a few problems: First you should avoid using 0.1 as coordinate step because it's a "bad number" in floating point (it's periodic when expressed in binary). Much better is 0.125 (1/8). Adding/substracting 1/8 will guarantee your numbers will always remain exact multiples of 1/8, not accumulating any error. You should define an "ok(x, y)" function checking if the (possibly fractional) point (x, y) is valid or inside a wall... a simple implementation could be: return map[y|0][x|0] == 0; // <expr>|0 is used to convert to integer Finally you should compute new_charX and new_charY and only accept moving from charX, charY to the new position if all four points: (new_charX+s, new_charY+s) (new_charX+1-s, new_charY+s) (new_charX+s, new_charY+1-s) (new_charX+1-s, new_charY+1-s) are valid with s = 1/16 (i.e. half of the moving step). Example: http://jsbin.com/wipimidije/edit?js,output
Your player can potentially be overlapping four squares in the map at any time. So you need to check for collision in all four squares (corresponding to the top-left, top-right, bottom-left, bottom-right corners of the character). To allow it to 'squeeze' through corridors (given that the character is the same size as a tile) you may also need to adjust this by one or two pixels (hence the 1 / config.tileSize in the following). function checkCollision( x, y ) { var x1 = Math.floor(x + 1 / config.tileSize), y1 = Math.floor(y + 1 / config.tileSize), x2 = Math.floor(x + 1 - 1 / config.tileSize), y2 = Math.floor(y + 1 - 1 / config.tileSize); if (map[y1][x1] !== 0 || map[y2][x1] !== 0 || map[y1][x2] !== 0 || map[y2][x2] !== 0) { return true; // Collision } return false; } See this version of the JSBin
My answer is a little different. Instead of having a 2D array I'd use a simple array and calculate the rows (if you even need them). Now you only have to check the map array at the one index that is the position of the actor: var map = [0,0,0,1,1,0,0,1,1,0,0,...]; //heres how to calculate the x/y if you need it for something else var pos_x = position % NUM_OF_ROWS; var pos_y = Math.floor(position / NUM_OF_ROWS); //for collisions now you just check the value of the array at that index leftKey.addEventListener('keypress', function() { test(position - 1); }); rightKey.addEventListener('keypress', function() { test(position + 1); }); upKey.addEventListener('keypress', function() { test(position + NUM_OF_ROWS); }); downKey.addEventListener('keypress', function() { test(position - NUM_OF_ROWS); }); function test(n) { if (map[n] === 0) { //if there's no collision, update the position. position = n; } else { console.log('Collided!'); } }
You need to consider two aspects: the first is collision detection, the second is response. Let's start with the detection. You are checking a single point, but in reality you have a tile size, so there is thickness which you must consider. The coordinate of the character, and the coordinate of your tiles is the top-left corner. It is not sufficient to compare the top-left corners, you must also check the other corners. The right-hand side of the player's square for example is at charX + config.tileSize. The second aspect is the collision response. The simplest mechanism you can use here is to check the next position of the character for collisions, and only move the character if there are none. You should preferably check the two axes separately to allow the character to "slide" along walls (otherwise it till get stuck in walls in moving diagonally into the wall).
First of all I would change the tiles "value" if you change the character to walk in 1's but in 0's you can check if a tile is walkable by typing If(tile[x][y])... Then, I would, first calculate the next position and then make the move if the player is able to... Var nextpos = new position; If(KEYLEFT){ Nextpos.x = currpos - 1; } If(nextpos > 0 && nextpos < mapsize && tile[nextpos.x][nextpos.y]) Player.pos = nextpos;
Javascript number/string issue
I am working with an application called TEC-IT Wedge to interface with a grocery scale (for those not in the know a grocery scale is a glorified scale with a scanner. The scale is connected to a serial port on the PC and thw wedge app monitors that port, the code below handles the scan of the barcode then requests the weight, all of this is working. Where I am running into the issue is formating the returning weight from the scale. the data returned from the scale is in the format of ASCII "S11####" so S110004 would be for an item weighing 0.04 pounds and this is how I need to send that wait to the application that we are using the scale to enter data with. For the code below a return of S110004 it returns 00.4 I think I am loosing 0's in the grab of the digits for some reason to place the decimal poit in the correct position. var cartonverify = Left(DATARAW, 5); var weightverify = Left(DATARAW, 3); if (cartonverify == "S08B3") { ActivateWindow("Untitled - Notepad"); SendKeyStrokes(DATARAW.replace(/^S08B3+/i, '') ); DATA = ""; WriteToDevice("S11\x0d", 1000); } if (weightverify == "S11") { ActivateWindow("Untitled - Notepad"); var cartwieght = Right(DATA, 4); if (cartwieght.length == 4) { var cartounces = Right(cartwieght, 2); var cartpounds = Left(cartwieght, 2); var WMWeight = cartpounds + "." + cartounces; SendKeyStrokes(WMWeight); DATA = ""; } }
Re-form your string to something that can be parsed as a number as desired. var str = 'S110004'; str = str.replace(/S11(\d\d)(\d\d)/, '$1.$2'); // "00.04" var num = parseFloat(str); // 0.04 Putting the decimal place in at the RegExp step also saves you from having to use division.
Base64 Image from encoding to display
I am trying to display an image on a web page that I get from Visual C++ via a pluggin. std::string DIBtoURI(LPBITMAPINFOHEADER lpbmih){ int width = lpbmih->biWidth; int height = lpbmih->biHeight; // height < 0 if bitmap is top-down if (height < 0) height = -height; // Populate the pixels array. unsigned char *bitmap = (unsigned char *) lpbmih + sizeof(BITMAPINFOHEADER); HBITMAP f = CreateBitmap(width,height,1,24,bitmap); BITMAP bit; GetObject(f, sizeof(BITMAP), &bit); // Allocate memory. BYTE* pBitmapData = new BYTE[ bit.bmWidth*bit.bmHeight ]; ZeroMemory( pBitmapData, bit.bmWidth*bit.bmHeight ); // Get bitmap data. GetBitmapBits(f, bit.bmWidth*bit.bmHeight, pBitmapData ); std::string tee = base64_encode(pBitmapData,bit.bmWidth*bit.bmHeight); return tee; } In my function, you can see I get a BITMAPINFOHEADER (from Twain), I create a BITMAP from it, and then I use a base64 encoder I have found on google : http://www.adp-gmbh.ch/cpp/common/base64.html I get a string I give to the web page and I'm trying to do var src="image/jpeg;base64," + string_from_cpp But it doesn't work ! I compared the string I get with my code, and what I get with a online encoder, and it's different, mine is very short. What am I doing wrong? Header problem ? I don't know how to encode the header of my image ?
You calculated the image size in the wrong way. Please try: ((((bit.bmWidth * 24)+ 31)/32)*4)*bit.bmHeight