Related
I have a following task that I'm trying to accomplish the most efficient way possible: I have varying number of pictures of varying size as pixel arrays that I need to add to canvas pixel by pixel. Each pixel's value has to be added to canvas's ImageData so that the result is a blend of two or more images.
My current solution is to retrieve ImageData from the location where the picture needs to be blended with the size of the picture. Then I add the picture's ImageData to the retrieved ImageData and copy it back to the same location.
In a sense this is a manual implementation of canvas globalCompositeOperation "lighter".
"use strict";
let canvas = document.getElementById("canvas");
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
let ctx = canvas.getContext("2d");
ctx.fillStyle="black";
ctx.fillRect(0, 0, width, height);
let imageData = ctx.getImageData(0,0,width,height);
let data = imageData.data;
function random(min, max) {
let num = Math.floor(Math.random() * (max - min + 1)) + min;
return num;
}
function createColorArray(size, color) {
let arrayLength = (size*size)*4;
let array = new Uint8ClampedArray(arrayLength);
for (let i = 0; i < arrayLength; i+=4) {
switch (color) {
case 1:
array[i+0] = 255; // r
array[i+1] = 0; // g
array[i+2] = 0; // b
array[i+3] = 255; // a
break;
case 2:
array[i+0] = 0; // r
array[i+1] = 255; // g
array[i+2] = 0; // b
array[i+3] = 255; // a
break;
case 3:
array[i+0] = 0; // r
array[i+1] = 0; // g
array[i+2] = 255; // b
array[i+3] = 255; // a
}
}
return array;
}
function picture() {
this.size = random(10, 500);
this.x = random(0, width);
this.y = random(0, height);
this.color = random(1,3);
this.colorArray = createColorArray(this.size, this.color);
}
picture.prototype.updatePixels = function() {
let imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
let data = imageData.data;
for (let i = 0; i < data.length; ++i) {
data[i]+=this.colorArray[i];
}
ctx.putImageData(imageData, this.x, this.y);
}
let pictures = [];
let numPictures = 50;
for (let i = 0; i < numPictures; ++i) {
let pic = new picture();
pictures.push(pic);
}
function drawPictures() {
for (let i = 0; i < pictures.length; ++i) {
pictures[i].updatePixels();
}
}
drawPictures();
<!DOCTYPE html>
<html>
<head>
<title>...</title>
<style type="text/css">
body {margin: 0px}
#canvas {position: absolute}
</style>
</head>
<body>
<div>
<canvas id="canvas"></canvas>
</div>
<script type="text/javascript" src="js\script.js"></script>
</body>
</html>
This solution works fine but it's very slow. I don't know if pixel by pixel blending can even be made very efficient, but one reason for slow performance might be that I need to get the ImageData and put it back each time a new image is blended into canvas.
Therefore the main question is how could I get whole canvas ImageData once in the beginning and then look correct pixels to update based on location and size of each picture that needs to blended into canvas and finally put updated ImageData back to canvas? Also, any other ideas on how to make blending more efficient are greatly appreciated.
Use the array methods.
The fastest way to fill an array is with the Array.fill function
const colors = new Uint32Array([0xFF0000FF,0xFF00FF00,0xFFFF00]); // red, green, blue
function createColorArray(size, color) {
const array32 = new Uint32Array(size*size);
array32.fill(colors[color]);
return array32;
}
Quick clamped add with |
If you are adding 0xFF to any channel and 0 to the others you can use | and a 32 bit array. For the updatePixels function
var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);
var i = 0;
var pic = this.colorArray; // use a local scope for faster access
do{
data[i] |= pic[i] ; // only works for 0 and FF chanel values
}while(++i < data.length);
ctx.putImageData(imageData, this.x, this.y);
Bitwise or | is similar to arithmetic add and can be used to increase values using 32bit words. The values will be clamped as part of the bitwise operation.
// dark
var red = 0xFF000088;
var green = 0xFF008800;
var yellow = red | green; // 0xFF008888
There are many other ways to use 32bit operations to increase performance as long as you use only 1 or 2 operators. More and you are better off using bytes.
You can also add if you know that each channel will not overflow a bit
a = 0xFF101010; // very dark gray
b = 0xFF000080; // dark red
// non overflowing add
c = a + b; // result is 0xFF000090 correct
// as 0x90 + 0x80 will overflow = 0x110 the add will not work
c += b; // result overflows bit to green 0xFF000110 // incorrect
Uint8Array V Uint8ClampedArray
Uint8Array is slightly faster than Uint8ClampedArray as the clamping is skipped for the Uint8Array so use it if you don't need to clamp the result. Also the int typedArrays do not need you to round values when assigning to them.
var data = Uint8Array(1);
data[0] = Math.random() * 255; // will floor for you
var data = Uint8Array(1);
data[0] = 256; // result is 0
data[0] = -1; // result is 255
var data = Uint8ClampedArray(1);
data[0] = 256; // result is 255
data[0] = -1; // result is 0
You can copy data from array to array
var imageDataSource = // some other source
var dataS = new Uint32Array(imageData.data.buffer);
var imageData = ctx.getImageData(this.x, this.y, this.size, this.size);
var data = new Uint32Array(imageData.data.buffer);
data.set(dataS); // copies all data
// or to copy a row of pixels
// from coords
var x = 10;
var y = 10;
var width = 20; // number of pixels to copy
// to coords
var xx = 30
var yy = 30
var start = y * this.size + x;
data.set(dataS.subArray(start, start + width), xx + yy * this.size);
Dont dump buffers
Don't keep fetching pixel data if not needed. If it does not change between putImageData and getImageData then there is no need to get the data again. It is better to keep the one buffer than continuously creating a new one. This will also relieve the memory stress and reduce the workload on GC.
Are you sure you can not use the GPU
And you can perform a wide range of operations on pixel data using global composite operations. Add, subtract, multiply, divide, invert These are much faster and so far in your code I can see no reason why you need to access the pixel data.
UPDATE: This entire issue ended up being a problem with the systems graphics driver, and not (seemingly) a browser / API issue. The torn frames came down to the actual display updating. Thank you again to those who were a part of the discussion and attempts to help.
I have page that uses a canvas and 2d context to display a pre-rendered frame at 720p. I'm rendering the frames separately and updating a variable with the new ImageData. Then, within requestAnimationFrame I simply do context.putImageData(cached_image_data);. Despite having the frame fully rendered in advance and effectively double buffered, I still get tearing far too often. There are a few other questions along these lines that I've found on SO, but they all end in "Use RAF". The code comes down to this:
var canvas = document.getElementById("canvas");
var cached_frame = new ImageData(new Uint8ClampedArray(canvas.width * canvas.height * 4), canvas.width, canvas.height);
var context = canvas.getContext("2d");
var framerate = 30;
function draw() {
if (cached_frame)
context.putImageData(cached_frame, 0, 0);
requestAnimationFrame(draw);
}
setInterval(function() {
var frame = context.getImageData(0, 0, canvas.width, canvas.height);
// Do things to manipulate frame.data.
// Save the resultant pixel data for the cached_frame.
cached_frame.data = frame.data;
}, 1000 / framerate);
draw();
Is there anything more that I can do without turning to webgl?
Any suggestions appreciated. TY all :D
I don't think the code is doing what you think it's doing
First off, as far as I know you can't assign new data to an ImageData so this line
cached_frame.data = frame.data;
Doesn't do anything. We can test that which shows it doens't work
var ctx = document.createElement("canvas").getContext("2d");
document.body.appendChild(ctx.canvas);
var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
var data = new Uint8ClampedArray(imageData.length);
// fill imageData.data with red
fillWithColor(255, 0, 0, 255, imageData.data);
// fill data with green
fillWithColor(0, 255, 0, 255, data);
// assign imageData.data to data
imageData.data = data;
// Draw. If assigning imageData.data works result will
// be green, if not result will be red
ctx.putImageData(imageData, 0, 0);
function fillWithColor(r, g, b, a, dst) {
for (ii = 0; ii < dst.length; ii += 4) {
dst[ii + 0] = r;
dst[ii + 1] = g;
dst[ii + 2] = b;
dst[ii + 3] = a;
}
}
Second, your draw function is drawing continuously, at least from the code you posted cached_frame is set on line 2 so it's always going to be true and always going to be drawing. If you're somehow partially updating the actual data in cached_frame then it's going to draw when there are only partial results.
I think you want something like this instead
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;
function draw() {
context.putImageData(frame, 0, 0);
}
setInterval(function() {
frame = context.getImageData(0, 0, canvas.width, canvas.height);
// Do things to manipulate frame.data
// frame is ready, draw it at next rAF
requestAnimationFrame(draw);
}, 1000 / framerate);
You might want to check if a draw it is already queued if you think decoding will ever happen faster than raf. I don't think you actually need rAF in this case though. I'm pretty sure you could just draw at the end of your setInterval and it will show up the next frame, no tearing.
Here's a test, it's not tearing for me.
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;
var frameCount = 0;
setInterval(function() {
++frameCount;
frame = context.getImageData(0, 0, canvas.width, canvas.height);
var data = frame.data;
var width = frame.width;
var height = frame.height;
// Do things to manipulate frame.data
for (var yy = 0; yy < height; ++yy) {
for (var xx = 0; xx < width; ++xx) {
var offset = (yy * width + xx) * 4;
data[offset + 0] = ((xx >> 2 & 0x1) ^ frameCount & 0x1) ? 255 : 0;
data[offset + 3] = 255;
}
}
// frame is ready, draw it at next rAF
context.putImageData(frame, 0, 0);
}, 1000 / framerate);
<canvas id="canvas" width="1280" height="720"></canvas>
I am trying to design a traveling sine wave in JavaScript, but the design appears quite slow. The main bottleneck is the clearRect() for canvas clearing.
How can I solve this?
Also I am drawing the pixel by ctx.fillRect(x, y,1,1), but when I clear using clearRect(x, y,1,1), it leaves some footprints. Instead I have to do clearRect(x, y,5,5) to get proper clearing. What can be the work around?
/******************************/
var x = 0;
var sineval = [];
var offset = 0;
var animFlag;
function init() {
for(var i=0; i<=1000; ++i){
sineval[i] = Math.sin(i*Math.PI/180);
}
// Call the sineWave() function repeatedly every 1 microseconds
animFlag = setInterval(sineWave, 1);
//sineWave();
}
function sineWave()
{ //console.log('Drawing Sine');
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
}
for(x=0 ; x<1000 ;++x){
// Find the sine of the angle
//var i = x % 361;
var y = sineval[x+offset];
// If the sine value is positive, map it above y = 100 and change the colour to blue
if(y >= 0)
{
y = 100 - (y-0) * 70;
ctx.fillStyle = "green";
}
// If the sine value is negative, map it below y = 100 and change the colour to red
if( y < 0 )
{
y = 100 + (0-y) * 70;
ctx.fillStyle = "green";
}
// We will use the fillRect method to draw the actual wave. The length and breath of the
if(x == 0) ctx.clearRect(0,y-1,5,5);
else ctx.clearRect(x,y,5,5);
ctx.fillRect(x, y,1,1 /*Math.sin(x * Math.PI/180) * 5, Math.sin(x * Math.PI/180 * 5)*/);
}
offset = (offset > 360) ? 0 : ++offset ;
}
You need to refactor the code a bit:
Move all global variables such as canvas and context outside of the loop function
Inside the loop, clear full canvas at beginning, redraw sine
Use requestAnimationFrame instead of setInterval
Replace fillRect() with rect() and do a single fill() outside the inner for-loop
Using a timeout value of 1 ms will potentially result in blocking the browser, or at least slow it down noticeably. Considering that a monitor update only happens every 16.7ms this will of course be wasted cycles. If you want to reduce/increase the speed of the sine you can reduce/increase the incremental step instead.
In essence:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var sineval = [];
var offset = 0;
init();
function init() {
for (var i = 0; i <= 1000; ++i) {
sineval.push(Math.sin(i * Math.PI / 180));
}
// Call the sineWave() function
sineWave();
}
function sineWave() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
ctx.fillStyle = "green";
// draw positive part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y >= 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "red";
// draw negative part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y < 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
offset = (offset > 360) ? 0 : ++offset;
requestAnimationFrame(sineWave);
}
<canvas id="canvas" width=800 height=500></canvas>
And of course, if you load the script in <head> you need to wrap it in a window.onload block so canvas element is available. Or simply place the script at the bottom of the page if you haven't already.
A few speedups and odd ends:
In init, set up the sine wave pixel values one time.
Use typed arrays for these since sticking with integers is faster than using floats if possible.
We will manipulate the pixel data directly instead of using fill and clear. To start this, in init we call ctx.getImageData one time. We also just one time max the alpha value of all the pixels since the default 0 value is transparent and we want full opacity at 255.
Use setInterval like before. We want to update the pixels at a steady rate.
Use 'adj' as knob to adjust how fast the sine wave moves on the screen. The actual value (a decimal) will depend on the drawing frame rate. We use Date.now() calls to keep track of milliseconds consumed across frames. So the adjustment on the millisecond is mod 360 to set the 'offset' variable. Thus offset value is not inc by 1 every frame but instead is decided based on the consumption of time. The adj value could later be connected to gui if want.
At end of work (in sineWave function), we call requestAnimationFrame simply to do the ctx.putImageData to the canvas,screen in sync to avoid tearing. Notice 'paintit' function is fast and simple. Notice also that we still require setInterval to keep steady pace.
In between setting the offset and calling requestAnimationFrame, we do two loops. The first efficiently blackens out the exact pixels we drew from the prior frame (sets to 0). The second loop draws the new sine wave. Top half of wave is green (set the G in pixel rgba to 255). Bottom half is red (set the R pixel rgba to 255).
Use the .data array to paint a pixel, and index it to the pixel using 4x + 4y*canvas.width. Add 1 more if want the green value instead of the red one. No need to touch the blue value (byte offset 2) nor the already set alpha (byte offset 3).
The >>>0 used in some places turns the affected value into an unsigned integer if it wasn't already. It can also be used instead of Math.ceil. .data is typed Array already I think.
This answer is rather late but it addresses some issues brought up in comments or otherwise not yet addressed. The question showed up during googling.
Code hasn't been profiled. It's possible some of the speedups didn't speed anything up; however, the cpu consumption of firefox was pretty light by the end of the adjustments. It's set to run at 40 fps. Make 'delay' smaller to speed it up and tax cpu more.
var sineval;
var offset = 0;
var animFlag;
var canvas;
var ctx;
var obj;
var milli;
var delay=25;
var adj=1/delay; // .04 or so for 25 delay
function init() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
obj=ctx.getImageData(0,0,canvas.width,canvas.height);
for (let i=0; i<obj.data.length; i+=4) {
obj.data[i+3]=255; //set all alpha to full one time only needed.
}
sineval=new Uint8Array(1400); //set up byte based table of final pixel sine values.. 1400 degrees total
for (let i=0; i<=1400; ++i) { //1400
sineval[i] = (100-70*Math.sin(i*Math.PI/180))>>>0;
}
animFlag = setInterval(sineWave, delay); //do processing once every 25 milli
milli=Date.now()>>>0; //start time in milli
}
function sineWave() {
let m=((Date.now()-milli)*adj)>>>0;
let oldoff = offset;
offset=(m % 360)>>>0; //offset,frequency tuned with adj param.
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+oldoff];
obj.data [0+x*4+y*4*canvas.width]=0; //black the reds
obj.data [1+x*4+y*4*canvas.width]=0; //black the greens
}
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+offset];
if (y<100) {
obj.data [1+x*4+y*4*canvas.width]=255; //rGba //green for top half
} else {
obj.data [0+x*4+y*4*canvas.width]=255; //Rgba //red for bottom half
}
}
requestAnimationFrame(paintit); //at end of processing try to paint next frame boundary
}
function paintit() {
ctx.putImageData(obj,0,0);
}
init();
<canvas id="canvas" height=300 width=1000></canvas>
I have a black canvas with things being drawn inside it. I want the things drawn inside to fade to black, over time, in the order at which they are drawn (FIFO). This works if I use a canvas which hasn't been resized. When the canvas is resized, the elements fade to an off-white.
Question: Why don't the white specks fade completely to black when the canvas has been resized? How can I get them to fade to black in the same way that they do when I haven't resized the canvas?
Here's some code which demonstrates. http://jsfiddle.net/6VvbQ/35/
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 150),
2, 2);
context.fillStyle = 'rgba(0,0,0,.02)';
context.fillRect(0, 0, 300, 150);
setTimeout('draw()', 1000 / 20);
}
setTimeout('draw()', 1000 / 20);
The problem is two-parted:
There is a (rather known) rounding error when you draw with low alpha value. The browser will never be able to get the resulting mix of the color and alpha channel equal to 0 as the resulting float value that is mixed will be converted to integer at the time of drawing which means the value will never become lower than 1. Next time it mixes it (value 1, as alpha internally is a value between 0 and 255) will use this value again and it get rounded to again to 1, and forever it goes.
Why it works when you have a resized canvas - in this case it is because you are drawing only half the big canvas to the smaller which result in the pixels being interpolated. As the value is very low this means in this case the pixel will turn "black" (fully transparent) as the average between the surrounding pixels will result in the value being rounded to 0 - sort of the opposite than with #1.
To get around this you will manually have to clear the spec when it is expected to be black. This will involve tracking each particle/spec yourselves or change the alpha using direct pixel manipulation.
Update:
The key is to use tracking. You can do this by creating each spec as a self-updating point which keeps track of alpha and clearing.
Online demo here
A simple spec object can look like this:
function Spec(ctx, speed) {
var me = this;
reset(); /// initialize object
this.update = function() {
ctx.clearRect(me.x, me.y, 1, 1); /// clear previous drawing
this.alpha -= speed; /// update alpha
if (this.alpha <= 0) reset(); /// if black then reset again
/// draw the spec
ctx.fillStyle = 'rgba(255,255,255,' + me.alpha + ')';
ctx.fillRect(me.x, me.y, 1, 1);
}
function reset() {
me.x = (ctx.canvas.width * Math.random())|0; /// random x rounded to int
me.y = (ctx.canvas.height * Math.random())|0; /// random y rounded to int
if (me.alpha) { /// reset alpha
me.alpha = 1.0; /// set to 1 if existed
} else {
me.alpha = Math.random(); /// use random if not
}
}
}
Rounding the x and y to integer values saves us a little when we need to clear the spec as we won't run into sub-pixels. Otherwise you would need to clear the area around the spec as well.
The next step then is to generate a number of points:
/// create 100 specs with random speed
var i = 100, specs = [];
while(i--) {
specs.push(new Spec(ctx, Math.random() * 0.015 + 0.005));
}
Instead of messing with FPS you simply use the speed which can be set individually per spec.
Now it's simply a matter of updating each object in a loop:
function loop() {
/// iterate each object
var i = specs.length - 1;
while(i--) {
specs[i].update(); /// update each object
}
requestAnimationFrame(loop); /// loop synced to monitor
}
As you can see performance is not an issue and there is no residue left. Hope this helps.
I don't know if i have undertand you well but looking at you fiddle i think that, for what you are looking for, you need to provide the size of the canvas in any iteration of the loop. If not then you are just taking the initial values:
EDIT
You can do it if you apply a threshold filter to the canvas. You can run the filter every second only just so the prefromanece is not hit so hard.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0,0,300,150);
//context.globalAlpha=1;
//context.globalCompositeOperation = "source-over";
var canvas2 = document.getElementById('canvas2');
var context2 = canvas2.getContext('2d');
canvas2.width=canvas2.height=canvas.width;
window.draw = function(){
var W = canvas2.width;
var H = canvas2.height;
context2.fillStyle='rgba(255,255,255,1)';
context2.fillRect(
Math.floor(Math.random()*W),
Math.floor(Math.random()*H),
2,2);
context2.fillStyle='rgba(0,0,0,.02)';
context2.fillRect(0,0,W,H);
context.fillStyle='rgba(0,0,0,1)';
context.fillRect(0,0,300,150);
context.drawImage(canvas2,0,0,300,150);
setTimeout('draw()', 1000/20);
}
setTimeout('draw()', 1000/20);
window.thresholdFilter = function () {
var W = canvas2.width;
var H = canvas2.height;
var i, j, threshold = 30, rgb = []
, imgData=context2.getImageData(0,0,W,H), Npixels = imgData.data.length;
for (i = 0; i < Npixels; i += 4) {
rgb[0] = imgData.data[i];
rgb[1] = imgData.data[i+1];
rgb[2] = imgData.data[i+2];
if ( rgb[0] < threshold &&
rgb[1] < threshold &&
rgb[2] < threshold
) {
imgData.data[i] = 0;
imgData.data[i+1] = 0;
imgData.data[i+2] = 0;
}
}
context2.putImageData(imgData,0,0);
};
setInterval("thresholdFilter()", 1000);
Here is the fiddle: http://jsfiddle.net/siliconball/2VaLb/4/
To avoid the rounding problem you could extract the fade effect to a separate function with its own timer, using longer refresh interval and larger alpha value.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 300),
2, 2);
setTimeout('draw()', 1000 / 20);
}
window.fadeToBlack = function () {
context.fillStyle = 'rgba(0,0,0,.1)';
context.fillRect(0, 0, 300, 300);
setTimeout('fadeToBlack()', 1000 / 4);
}
draw();
fadeToBlack();
Fiddle demonstrating this: http://jsfiddle.net/6VvbQ/37/
Our company website features a "random shard generator", built in Flash, which creates a number of overlapping coloured shard graphics at random just below the site header.
http://www.clarendonmarketing.com
I am trying to replicate this effect using HTML5, and whilst I can generate the random shards easily enough, the blended overlapping (multiply in Adobe terms) is proving a challenge.
I have a solution which basically creates an array of all the canvas's pixel data before each shard is drawn, then another array with the canvas's pixel data after each shard is drawn. It then compares the two and where it finds a non transparent pixel in the first array whose corresponding pixel in the second array matches the currently selected fill colour, it redraws it with a new colour value determined by a 'multiply' function (topValue * bottomValue / 255).
Generally this works fine and achieves the desired effect, EXCEPT around the edges of the overlapping shards, where a jagged effect is produced.
I believe this has something to do with the browser's anti-aliasing. I have tried replicating the original pixel's alpha channel value for the computed pixel, but that doesn't seem to help.
Javascript:
// Random Shard Generator v2 (HTML5)
var theCanvas;
var ctx;
var maxShards = 6;
var minShards = 3;
var fillArray = new Array(
[180,181,171,255],
[162,202,28,255],
[192,15,44,255],
[222,23,112,255],
[63,185,127,255],
[152,103,158,255],
[251,216,45,255],
[249,147,0,255],
[0,151,204,255]
);
var selectedFill;
window.onload = function() {
theCanvas = document.getElementById('shards');
ctx = theCanvas.getContext('2d');
//ctx.translate(-0.5, -0.5)
var totalShards = getRandom(maxShards, minShards);
for(i=0; i<=totalShards; i++) {
//get snapshot of current canvas
imgData = ctx.getImageData(0,0,theCanvas.width,theCanvas.height);
currentPix = imgData.data
//draw a shard
drawRandomShard();
//get snapshot of new canvas
imgData = ctx.getImageData(0,0,theCanvas.width,theCanvas.height);
pix = imgData.data;
//console.log(selectedFill[0]+','+selectedFill[1]+','+selectedFill[2]);
//alert('break')
//CALCULATE THE MULTIPLIED RGB VALUES FOR OVERLAPPING PIXELS
for (var j = 0, n = currentPix.length; j < n; j += 4) {
if (
//the current pixel is not blank (alpha 0)
(currentPix[j+3]>0)
&& //and the new pixel matches the currently selected fill colour
(pix[j]==selectedFill[0] && pix[j+1]==selectedFill[1] && pix[j+2]==selectedFill[2])
) { //multiply the current pixel by the selected fill colour
//console.log('old: '+currentPix[j]+','+currentPix[j+1]+','+currentPix[j+2]+','+currentPix[j+3]+'\n'+'new: '+pix[j]+','+pix[j+1]+','+pix[j+2]+','+pix[j+3]);
pix[j] = multiply(selectedFill[0], currentPix[j]); // red
pix[j+1] = multiply(selectedFill[1], currentPix[j+1]); // green
pix[j+2] = multiply(selectedFill[2], currentPix[j+2]); // blue
}
}
//update the canvas
ctx.putImageData(imgData, 0, 0);
}
};
function drawRandomShard() {
var maxShardWidth = 200;
var minShardWidth = 30;
var maxShardHeight = 16;
var minShardHeight = 10;
var minIndent = 4;
var maxRight = theCanvas.width-maxShardWidth;
//generate a random start point
var randomLeftAnchor = getRandom(maxRight, 0);
//generate a random right anchor point
var randomRightAnchor = getRandom((randomLeftAnchor+maxShardWidth),(randomLeftAnchor+minShardWidth));
//generate a random number between the min and max limits for the lower point
var randomLowerAnchorX = getRandom((randomRightAnchor - minIndent),(randomLeftAnchor + minIndent));
//generate a random height for the shard
var randomLowerAnchorY = getRandom(maxShardHeight, minShardHeight);
//select a fill colour from an array
var fillSelector = getRandom(fillArray.length-1,0);
//console.log(fillSelector);
selectedFill = fillArray[fillSelector];
drawShard(randomLeftAnchor, randomLowerAnchorX, randomLowerAnchorY, randomRightAnchor, selectedFill);
}
function drawShard(leftAnchor, lowerAnchorX, lowerAnchorY, rightAnchor, selectedFill) {
ctx.beginPath();
ctx.moveTo(leftAnchor,0);
ctx.lineTo(lowerAnchorX,lowerAnchorY);
ctx.lineTo(rightAnchor,0);
ctx.closePath();
fillColour = 'rgb('+selectedFill[0]+','+selectedFill[1]+','+selectedFill[2]+')';
ctx.fillStyle=fillColour;
ctx.fill();
};
function getRandom(high, low) {
return Math.floor(Math.random() * (high-low)+1) + low;
}
function multiply(topValue, bottomValue){
return topValue * bottomValue / 255;
};
Working demo:
http://www.clarendonmarketing.com/html5shards.html
Do you really need multiplication? Why not just use lower opacity blending?
Demo http://jsfiddle.net/wk3eE/
ctx.globalAlpha = 0.6;
for(var i=totalShards;i--;) drawRandomShard();
Edit: If you really need multiplication, then leave it to the professionals, since multiply mode with alpha values is a little tricky:
Demo 2: http://jsfiddle.net/wk3eE/2/
<script type="text/javascript" src="context_blender.js"></script>
<script type="text/javascript">
var ctx = document.querySelector('canvas').getContext('2d');
// Create an off-screen canvas to draw shards to first
var off = ctx.canvas.cloneNode(true).getContext('2d');
var w = ctx.canvas.width, h = ctx.canvas.height;
for(var i=totalShards;i--;){
off.clearRect(0,0,w,h); // clear the offscreen context first
drawRandomShard(off); // modify to draw to the offscreen context
off.blendOnto(ctx,'multiply'); // multiply onto the main context
}
</script>