📜 ⬆️ ⬇️

Jpg, transparency, canvas, animation

Hello, friends!

I offer you a small lesson on the theme of animation of sprites with alpha channel on HTML5 canvas.

Preamble.


First draw something
image
')
Why round and yellow? Because Douglas Adams in “The Hitchhiker's Guide to the Galaxy” has this Slartibartfast - a very touching uncle, has a prize for the coastlines in the construction of the Earth. Therefore, just in case, we will animate the yellow star.

Next, we animate the star in a sequence of 256 frames with a frame size of 256x256 pixels.
Those 256, these 256 and 256 are taken only for the sake of example. Animation can be both shorter and smaller, however, as well as longer and larger - it all depends on your goals. I assume that 256x256 pixels now is such a quite normal size for a sprite: not too big and not too small. Of course, earlier, when the grass was greener, and the sprites were more modest than 16x16 or 32x32, but we will be experimenting as if for growth.
256 frames is also not so much, but not so much: depending on the frame rate, you can get a 10-20-30 second sequence.

Until now, all without surprises, until you multiply the dimensions of the sides of the frame, duration and depth:

256 x 256 x 256 x 4 = 64 MB

But! 64 megabytes without compression is a little bit fat for the sprite. If you skip all 256 frames into a 16x16 matrix and save this fish to PNG with an alpha channel (everything happens in Adobe After Effects), you will get 18.6 MB at the output, which, although not 64, is still a lot. For those who are interested to look at this PNG here is a reference .

Squeeze!


Does After Effect really carelessly about PNG compression? To bring the resulting grid to a sane size, I tried to optimize the rendered PNG using the OptiPNG and PNGOut utilities. Both utilities were run in with various options up to extreme, but as a result, in the best case, compression was optimized to an enchanting 1.5 percent, which clearly does not meet the expectations. Therefore, this animation was converted into a couple of files without loss of quality (the same PNG) but separately RGB and Alpha, and then using Image Optimizer, they were converted into a pair of JPEGs. To facilitate the work of the JPG compressor (is this the right word?), The background on the sequence that contains the RGB layers before rendering is forcibly filled with cosmic yellow instead of cosmic black.

Depending on the degree of compression, we get pairs of JPEGs of different weights:
Compressionten%15%25%50%75%85%90%
Rgb139KB198KB250KB425KB691KB992KB1337KB
Alpha363KB459KB524KB689KB856KB1067KB1273KB

These dimensions are closer to what you want. For me, it turned out to be a complete surprise that the file with transparency turns out to be heavier in all cases than the file with colors, probably the whole thing is in contrast. Visually, all that is compressed is stronger than 50% similar to bricks and not to Kosomes. Friends, if among you there is someone who can make a well compressed and at the same time well distinguishable JPEG - please share your experience, I will gladly update the article. In the meantime, further experiments will be carried out with a pair of files compressed to 75% that pull in the amount of 1547KB which is significantly better (lighter) than 18.6MB for PNG and even more so 64MB for the uncompressed format.

Forward!


Now these separate JPEGs will be given to us by the server and on the client we will receive and glue them into one common image with transparency.

The markup will be the most empty one:
Markup
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Canvas Example: Using canvas</title> <link rel="stylesheet" href="css/main.css" type="text/css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script> <script src="js/main.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function(){ loadRGBA("jpeg_16x16/render_16_16_rgb_75.jpg", "jpeg_16x16/render_16_16_aplha_75.jpg"); }); </script> </head> <body> <div id="star1" class="base64"></div> <div id="star2" class="base64"></div> </body> </html> 


As well, and styles:
Styles
 * { margin: 0; padding: 0; } html { background: #888 url(../backs/back64dark.png); font-size: 14px; font-family: 'Helvetica', Helvetica, sans-serif; } h1 { font-size: 1.5em; text-align: center; margin: 1em 0; color: #ddd; } #base64{ background: transparent; color: #bbb; width: 256px; height: 265px; margin: 0 auto; } 


In the script block at the end of the formation of the document call the function of loading images.
It is very simple (it is in the js / main.js file):
function loadRGBA ()
 function loadRGBA(url_rgb, url_alpha){ var img_rgb = new Image(); var img_alpha = new Image(); var img_count = 0; var img_rgba = ''; img_rgb.src = url_rgb; img_alpha.src = url_alpha; img_rgb.onload = function(){ ++img_count; if(2 == img_count){ img_rgba = compileRGBA(img_rgb, img_alpha); } } img_alpha.onload = function(){ ++img_count; if(2 == img_count){ img_rgba = compileRGBA(img_rgb, img_alpha); } } } 


In a nutshell: let's declare two Image objects and wait for them to load (that is, when the number img_count is 2). In my script this moment is made somewhat through one place - after loading any of the files it is checked and whether the ALREADY has loaded another one and, if so, the function of assembling two images into one is called. I'm not so good at JavaScript I just feel like something is wrong, but despite my feelings, it works. And since it is expected (in the example) always exactly 2 images, we’ll leave the loading function alone and write the assembly functions.

Collect!


compileRGBA ()
 function compileRGBA(raw_rgb, raw_alpha){ if (!raw_rgb.width || !raw_rgb.height || !raw_alpha.width || !raw_alpha.height){ return; } if (raw_rgb.width !== raw_alpha.width || raw_rgb.height !== raw_alpha.height){ alert(' RGB    ') return; } var canvas_rgb = document.createElement("canvas"); var canvas_alpha = document.createElement("canvas"); var canvas_frame = document.createElement("canvas"); if (!canvas_rgb || !canvas_rgb.getContext('2d') || !canvas_alpha || !canvas_alpha.getContext('2d') || !canvas_frame || !canvas_frame.getContext('2d')){ alert('----, ... ---, , '); return; } canvas_rgb.width = raw_rgb.width; canvas_rgb.height = raw_rgb.height; canvas_alpha.width = raw_alpha.width; canvas_alpha.height = raw_alpha.height; canvas_frame.width = 256; canvas_frame.height = 256; var context_rgb = canvas_rgb.getContext('2d'); var context_alpha = canvas_alpha.getContext('2d'); var context_frame = canvas_frame.getContext('2d'); context_rgb.drawImage(raw_rgb, 0, 0); context_alpha.drawImage(raw_alpha, 0, 0); var pix_rgb = context_rgb.getImageData(0, 0, raw_rgb.width, raw_rgb.height); var pix_alpha = context_alpha.getImageData(0, 0, raw_alpha.width, raw_alpha.height); for (var i = 0, n = pix_rgb.width * pix_rgb.height * 4; i < n; i += 4){ pix_rgb.data[i+3] = pix_alpha.data[i]; } context_rgb.putImageData(pix_rgb, 0, 0); var img_arr = []; var frames = []; for(var i=0; i<=15; i++){ for(var j=0; j<=15; j++){ frames[j*16 + i] = context_rgb.getImageData(i*256, j*256, 256, 256); } } var frame = 0; $("#base64").append(canvas_frame); var intFPS = setInterval(function(){ ++frame; if (frame > 255){ frame = 0; } context_frame.putImageData(frames[frame], 0, 0) }, 1000 / 16); } 


Here, too, there is nothing difficult - in the first condition the presence of dimensions is checked and in the second condition their conformity. then we declare three canvas: for color, for transparency and for animation. As it should be, we have the presence of support for the Canvas mechanism in the browser (I tested it in Mozilla Firefox and Google Chrome, everything is fine here and IE8 got hysterical and I left it alone. As dasm32 suggests: “Opera works” , and sashagil “in IE 10 (which is only in Win8 at the moment), normally shows,” but Krovosos “didn’t work Safari on iPad, even the background didn’t seem” ) and generate contexts for them. And then the magic begins:
  var pix_rgb = context_rgb.getImageData(0, 0, raw_rgb.width, raw_rgb.height); var pix_alpha = context_alpha.getImageData(0, 0, raw_alpha.width, raw_alpha.height); 

these two lines pull out the RGBA layers from the contexts of the color file and the transparency file, each of which expands into a 64-megabyte array, despite the fact that in the case of a color file there is no component of transparency, and in the case of a Gray-Mode transparency file, one is enough bit layer, but such is selyavi - the canvas is always 4 bytes deep. One byte on each of the three colors and one more for transparency. Once on the outline, the monochrome transparency file “turns into depth” by 32 bits in such a way that R = G = B and transparency = 255.

loop through all the pixels of these arrays and
 pix_rgb.data[i+3] = pix_alpha.data[i] 

Copy the red byte value from the transparency canvas to the transparency layer (data [i] is red, data [i + 1] is green, data [i + 2] is blue, data [i + 3] is alpha)

From this moment we have a huge (4096x4096) footcloth consisting of 256 individual frames, each of which is 256x256 pixels in size. The frames are laid out in a 16x16 grid from top to bottom and from left to right. Now it is the most logical to export this grid from the canvas to a document as a Base64 instance and feed it to the Background-Image property in a DIV of the appropriate size and shift the starting point of the timer by the frame size. The experiment showed that this is a terrible option in terms of CPU usage, my DualCore 2.2GHz could not deliver above 5 FPS. Therefore, we cut these 16x16 suns into frames and put them into an array.
  var frames = []; for(var i=0; i<=15; i++){ for(var j=0; j<=15; j++){ frames[j*16 + i] = context_rgb.getImageData(i*256, j*256, 256, 256); } } 

then we start the timer 16 times per second (it can be faster and slower) and see the animation , or download and watch locally.

Attention! Chrome will not locally “upload” files for reasons of ugliness, therefore, FF remains.
UPD SHVV : "... so that Chrome can open local files, it must be run with the --disable-web-security key" .

If you are watching online, then I must say that the download (1.5MB) happens unnoticed, but the gluing freezes for about 5 seconds, but then freezes and the animation works. Moreover, the processor is loaded at 1%, which gives reason to conduct a further experiment with a pair of such suns on the subject of "see how they intersect."

Cosmos 2.0


for this we modify CSS
New styles
 .base64{ background: transparent; position: absolute; width: 256px; height: 265px; margin: 0; } 


We do absolute positioning so that the block can fly.

And also javascript. First, we will make two stars, respectively, two DIVs, let's call two gluing functions -
modification loadRGBA ()
  img_rgb.onload = function(){ ++img_count; if(2 == img_count){ compileRGBA(img_rgb, img_alpha, "star1"); compileGGAA(img_rgb, img_alpha, "star2"); } } img_alpha.onload = function(){ ++img_count; if(2 == img_count){ compileRGBA(img_rgb, img_alpha, "star1"); compileGGAA(img_rgb, img_alpha, "star2"); } } 


one is the same as before (compileRGBA) and the second modified (compileGGAA) it differs from the first only in a slightly more free way of handling the color-transparency channels:
  pix_rgb.data[i] = pix_rgb.data[i+1]; pix_rgb.data[i+2] = pix_rgb.data[i+3] = pix_alpha.data[i]; 

as seen here in the loop, the value of the red channel is replaced with the value of green, and the blue and transparency are copied from the transparency of the second file. The star is turning blue. And also in the animation timer the starting frame value is shifted by a hundred. This is to not get synchronized swimming.

Second, let's set the timers to move these stars in an ellipse with a delta phase of 180 degrees:
timers
  var intRotate = setInterval(function(){ phase += .01; if (phase >= 6.28319) { phase = .0; } $("#star1.base64").css('top', (doc_h + 50*Math.sin(phase)) + 'px' ); $("#star2.base64").css('top', (doc_h + 50*Math.sin(phase + 3.14159)) + 'px' ); $("#star1.base64").css('left', (doc_w + 100*Math.cos(phase)) + 'px' ); $("#star2.base64").css('left', (doc_w + 100*Math.cos(phase + 3.14159)) + 'px' ); $("#star1.base64").css('z-index', (phase < 3.14159) ? '1001':'1000' ); $("#star2.base64").css('z-index', (phase < 3.14159) ? '1000':'1001' ); }, 1000 / 24); 


third, we remove intermediate structures:
remove trash
  delete pix_rgb; delete pix_alpha; delete context_rgb; delete canvas_rgb; delete context_alpha; delete canvas_alpha; 


It’s hard to say if there’s any sense from these deletions, at least the Task Manager didn’t notice any difference with the deletion without deletion.

Thus it turns out two alternately eclipsed stars.

- Can I see everyone?
- Yes of course. Will you download?

Summary


At this experiment / lesson is over. The feeling is twofold: on the one hand, everything works fundamentally. On the other - this wild brake at the time of gluing spoils the joy of victory. And what to do with it is not yet clear. If you have any suggestions, I will eagerly listen and make corrections.

Thank you for your attention, friends!

PS I smneyl for you background less merciless =)

Source: https://habr.com/ru/post/154103/


All Articles