In this acrticle, why in the loop is i incremented by 4 instead of by 1?. I tried changong i+=4 to i++ but it doesn't work properly. Could you please tell me what is the reason behind?
function grayScale(context, canvas) {
var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
var pixels = imgData.data;
for (var i = 0, n = pixels.length; i < n; i += 4) {
var grayscale = pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11;
pixels[i ] = grayscale; // red
pixels[i+1] = grayscale; // green
pixels[i+2] = grayscale; // blue
//pixels[i+3] is alpha
}
//redraw the image in black & white
context.putImageData(imgData, 0, 0);
}
//add the function call in the imageObj.onload
imageObj.onload = function(){
context.drawImage(imageObj, destX, destY);
grayScale(context, canvas);
};
Look at the actual code, they are using pixels[i] but also pixels[i+1], pixels[i+2] and a commented out pixels[i+3]. These are four values at a time, not just one.
In this context of pixels from an image it is actually very common to increment by four (or three if alpha is completely absent in the data) since the order comes in as RGBA.
If you look at the comments they even point this fact out (stripped of unnecessary parts):
pixels[i ] // red
pixels[i+1] // green
pixels[i+2] // blue
pixels[i+3] // alpha
If you consider the layout in the array it makes a whole lot of sense:
Array: [r0,g0,b0,a0,r1,g1,b1,a1,r2,...etc]
Positions: 0, 1, 2, 3, 4, 5, 6, 7, 8
We need to go from 0 to 4 to 8 if we are to always get red first.
It looks to me like i=red, i+1=green, i+2=blue, and i+3=alpha. So i+4 would be red again. So the for loop iterates across the array by four instead of one to distinguish separate pixels.
Because what happens in the for-loop is modifying four spots in the array which represent the red, green, blue, and alpha channels which make up a single pixel. The for-loop increments by 4 to jump to the next set of red, green, blue, and alpha channels for the next pixel.
Each iteration of the loop operates on a sequence of four items from the collection, starting on the current value of i.
Therefore the beginning of the next sequence is i + 4.
An ImageData object is basically an array of pixels.
In CSS, when you set color : rgba( 255, 40, 30, 1 ); you're setting red, green, blue and alpha(transparency).
That's the order that each pixel is.
But ALL pixels are stored as four colours, back to back in a straight line.
So a 2x2 black and white image looks like this:
var checkerboard = [ 0, 0, 0, 1, 255, 255, 255, 1, 255, 255, 255, 1, 0, 0, 0, 1 ];
That's the 3 rgbs to make black in the top-left pixel (and 1 for 100% visible), followed by white in the top-right, followed by white in the bottom-left, followed by black in the bottom right.
So when you're dealing with code which modifies individual pixels, you're including ALL of the colour/alpha channels which make up 1 pixel.
Related
I'm trying to figure out a way to punch holes into a thing, but without the hole also going through whatever is in the background already.
the hole is made of a few arbitrary shapes, and is not a simple path I can use to clip.
The hole is only punched through the foreground shapes, and not all the way into the background (the background should stay as-is).
I figured a way to do this with an external context, and then bringing it in.
My questions: is there a way to do it on my default canvas, and avoid the complications that might arise from the external context (extra memory, color differences etc)?
Here's a working (p5.js) example, which is using a new context:
function setup() {
createCanvas(600,600);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
pg = createGraphics(600,600);
//yellow+green foreground shapes
pg.fill("green");
pg.rect(100, 100, 200, 200);
pg.fill("yellow");
pg.rect(80, 80, 100, 300);
//punch a hole in the shapes
pg.fill(0, 0, 255);
pg.blendMode(REMOVE);
pg.circle(140, 140, 150);
pg.circle(180, 180, 150);
//bring in the external canvas with punched shapes
image(pg, 0, 0);
noLoop();
}
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
There is no easy or built in way to do this without the technique you've already discovered. The only alternative would be to implement boolean geometry operations like subtraction and intersection on arbitrary shapes and splines. That would allow you to make arbitrary bezier splines that represent the composites of multiple complex shapes and then draw those directly. This approach would have different behavior with regards to stroke than the removal approach.
Just FYI, there are also a pair of methods in p5js erase() and noErase() which have a similar behavior to the blendMode(REMOVE) approach. I don't think there's any technical benefit, but it might be more idiomatic to use them rather than blend mode.
I agree, as Paul(+1) mentions as well, using multiple p5.Graphics instances (external contexts as you call them) is the most straight forward/readable method.
You could explicitly uses p5.Image and mask(), however there are few more operations involved and the could would be a little less readable. Here's an example:
function setup() {
createCanvas(600,600);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
pg = createGraphics(600,600);
//yellow+green foreground shapes
pg.fill("green");
pg.rect(100, 100, 200, 200);
pg.fill("yellow");
pg.rect(80, 80, 100, 300);
//punch a hole in the shapes
let msk = createGraphics(600, 600);
msk.background(0);
msk.erase();
msk.noStroke();
msk.circle(140, 140, 150);
msk.circle(180, 180, 150);
let mskImage = msk.get();
pgImage = pg.get();
pgImage.mask(mskImage);
image(pgImage, 0, 0);
noLoop();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
A (very) hacky workaround would be to do the same thing with one canvas.
This would leave the areas inside the circles completely transparent so make them appear blue, simply make the background element behind the blue:
function setup() {
createCanvas(600,600);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
// pg = createGraphics(600,600);
//yellow+green foreground shapes
fill("green");
rect(100, 100, 200, 200);
fill("yellow");
rect(80, 80, 100, 300);
//punch a hole in the shapes
fill(0, 0, 255);
blendMode(REMOVE);
circle(140, 140, 150);
circle(180, 180, 150);
//bring in the external canvas with punched shapes
// image(pg, 0, 0);
noLoop();
}
body{
/* make the HTML background match the canvas blue */
background-color: #00F;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
This might not be flexible enough though.
Now, assuming your foreground is made of the yellow and green shapes and the background is blue, another option would be manually accessing the pixels[] array and updating pixel values. In your example the masks are circular so you could check if:
the distance between the current pixel and the circle's centre is smaller than the circle's radius: this means the pixel is inside the circle
also, if the colour inside the circle is a foreground colour (e.g. green or yellow in your case)
If both conditions match then you could replace this pixel with a background colour (blue in your case)
Here's an example of that:
function setup() {
createCanvas(600,600);
pixelDensity(1);
background(255, 0, 0);
noStroke();
}
function draw() {
//blue: stuff in the background that should not change
fill ("blue");
rect (20,20,500,500);
//draw on external canvas
//yellow+green foreground shapes
fill("green");
rect(100, 100, 200, 200);
fill("yellow");
rect(80, 80, 100, 300);
//punch a hole in the shapes
fill(0, 0, 255);
// make pixels available for reading
loadPixels();
// apply each circle "mask" / bg color replacement
// yellow , green , bg blue to replace fg with
circleMask(140, 140, 150, [255, 255, 0], [0, 0x80, 0], [0, 0, 255]);
circleMask(180, 180, 150, [255, 255, 0], [0, 0x80, 0], [0, 0, 255]);
// once all "masks" are applied,
updatePixels();
noLoop();
}
function circleMask(x, y, radius, fg1, fg2, bg){
// total number of pixels
let np = width * height;
let np4 = np*4;
//for each pixel (i = canvas pixel index (taking r,g,b,a order into account)
// id4 is a quarter of "i"
for(let i = 0, id4 =0 ; i < np4; i+=4, id4++){
// compute x from pixel index
let px = id4 % width;
// compute y from pixel index
let py = id4 / width;
// if we're within the circle
if(dist(px, py, x, y) < radius / 2){
// if we've found foreground colours to make transparent
// ([0][1][2] = r, g, b)
if((pixels[i] == fg1[0] || pixels[i] == fg2[0]) &&
(pixels[i+1] == fg1[1] || pixels[i+1] == fg2[1]) &&
(pixels[i+2] == fg1[2] || pixels[i+2] == fg2[2])){
// "mask" => replace fg colour matching pixel with bg pixel
pixels[i] = bg[0];
pixels[i+1] = bg[1];
pixels[i+2] = bg[2];
}
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.min.js"></script>
There are a few things to notice here.
pixels[] is faster than set(x, y, clr) but it means you need to remember a few details:
call loadPixels() before accessing pixels[] to read/populate the array
make all the pixels changes required (in this case the circle "masks" / pixels inside circle colour replacement)
call updatePixels() after pixels[] have been updated
Also notices it takes a bit of time to execute.
There could be a few speed improvements such as only iterating over the pixels inside the bounding box of the circle before checking distance and checking squared distance instead of dist(), however this would also make the code less readable.
I need to access individual pixel values of the live canvas which has a pattern(shapes) drawn from user input.
like following,
function draw(){
stroke(255);
if (mouseIsPressed == true) {
if(mouseX != old_mX || mouseY != old_mY)
{
obstacle[obstacle.length] = [mouseX, mouseY];
//line(mouseX, mouseY, pmouseX, pmouseY);
old_mX = mouseX;
old_mY = mouseY;
}
}
fill(255);
beginShape();
for(i = 0; i < obstacle.length;i++){
vertex(obstacle[i][0],obstacle[i][1]);
}
endShape();
}
After drawing is done need to access the individual pixel values
function keyTyped() {
if ( key == 'n')
{
obstacle = []
}
if( key == 'd'){
loadPixels();
//rest of the action
updatePixels();
}}
problem is that loadPixels(); does not loading array with correct values, loaded array is more like a random one containing random patten
is there correct way to access the pixels ?
I tried out your code, and it looks like it loads the pixels into pixels[] normally. I think the problem might be your expectation of 'normal'.
According to https://p5js.org/reference/#/p5/pixels:
The first four values (indices 0-3) in the array will be the R, G, B, A values of the pixel at (0, 0). The second four values (indices 4-7) will contain the R, G, B, A values of the pixel at (1, 0).
So, if you run loadPixels() on completely black canvas, you'll get the following array:
console.log(pixels);
-> [0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255...]
0 red, 0 green, 0 blue, 255 alpha in the first pixel, then
0 red, 0 green, 0 blue, 255 alpha in the second pixel, then...
Alpha is a measure of how transparent a pixel is. 255 means it is fully opaque, 0 means it is fully transparent.
Is there some kind of trick for creating a gradient with multiple stops in 2d space? What I would like to do is create a rectangle in my canvas, and then have a different color stop at each corner.
I've tried creating 4 gradients, one at each corner, that points to the opposite corner. (tried linear and circular). but that doesn't produce the effect I want because the center is always off color in a circular manner.
The effect I want would be similar to having 2 linear horizontal gradients placed on top of each other. and then a 3rd vertical linear gradient which doesn't affect the colors of the first two gradients but just fades the bottom gradient into the top gradient as it goes down. so that the top two corners are the first linear gradient and the bottom two corners are the second linear gradient.
I've tried playing around with globalCompositeOperation but the closest I've been able to achieve is a 3 sided gradient. not 4.
The only method I can think of for doing this is building my rectangle one line at a time. with each line having a linear gradient that is recalculated and changed slightly so that it's the second gradient by the time to draws the bottom line. but that doesn't seem like the most efficient (or easiest to program) way to do it.
Pictures
I guess I'm too new here to link to photos. But here is a link to 3 images on my google photos.
https://plus.google.com/photos/116764547896287695338/albums/6094993027344790369?authkey=CJnZ0I3JxbOMag
image 1: First Gradient (setting the top two corner colors)
image 2: Second Gradient (setting the bottom two corner colors)
image 3: Both image 1 & 2 blended together vertically so that the top two colors fade into the bottom two colors.
There is no direct operation for something like this. In certain special cases (ie. colorpicker) you can get away with two separate gradients.
If you want arbitrary color per corner, you will have to perform it pixel-wise. In other words do linear interpolation on both x and y for each pixel. I've done something like this for triangle gradients myself.
Compared to that four corner variant without any special transformation would be simple. You would simply have to go through each x and y of the canvas while keeping track of the state (two lerps per pixel).
Code Example
HTML
<canvas id="canvas" width="300" height="300"></canvas>
JS
main();
function main() {
var canvas = document.getElementById("canvas");
quadGradient(canvas, {
topLeft: [1, 1, 1, 1],
topRight: [0, 0, 0, 1],
bottomLeft: [0, 0, 0, 1],
bottomRight: [1, 1, 1, 1]
});
}
function quadGradient(canvas, corners) {
var ctx = canvas.getContext('2d');
var w = canvas.width;
var h = canvas.height;
var gradient, startColor, endColor, fac;
for(var i = 0; i < h; i++) {
gradient = ctx.createLinearGradient(0, i, w, i);
fac = i / (h - 1);
startColor = arrayToRGBA(
lerp(corners.topLeft, corners.bottomLeft, fac)
);
endColor = arrayToRGBA(
lerp(corners.topRight, corners.bottomRight, fac)
);
gradient.addColorStop(0, startColor);
gradient.addColorStop(1, endColor);
ctx.fillStyle = gradient;
ctx.fillRect(0, i, w, i);
}
}
function arrayToRGBA(arr) {
var ret = arr.map(function(v) {
// map to [0, 255] and clamp
return Math.max(Math.min(Math.round(v * 255), 255), 0);
});
// alpha should retain its value
ret[3] = arr[3];
return 'rgba(' + ret.join(',') + ')';
}
function lerp(a, b, fac) {
return a.map(function(v, i) {
return v * (1 - fac) + b[i] * fac;
});
}
JSbin
i drew an image using drawImage() api of html5 canvas. now i want to fill the white spaces of that drew image with different colors (individual box individual color). how can i do it? i am using html5 canvas and jquery.
i want to fill the white spaces with different color and those white spaces are not proper rectangular box.
thanks in advance.
[ changed answer after clarification from questioner ]
Given: an image that has many fully-enclosed transparent areas.
This is a method to fill each transparent area with a different color:
Use context.getImageData to get an array of each canvas pixels [r,g,b,a] value.
Loop thru the array and find the first transparent pixel (the "a" value ==0)
Floodfill the entire transparent area containing that pixel with a new opaque color (replace the r,g,b values with your new color and replace the "a" value ==255).
Repeat step#2 until all the transparent areas have been filled with new unique colors.
To get you started...
William Malone wrote a very nice article on how to get and use canvas's [r,g,b,a] color array.
His article also shows you how to "floodfill" -- replace an existing color with a new color in an entire contiguous area.
In your case, you would replace transparent pixels with a new color.
This is his article:
http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/
[Addition to question: insert images into areas ]
You need to make each colorized area transparent again—one at a time
If you save each area's starting pixel from when you originally colorized the areas, you can start with that pixel and re-floodfill an area. This time you’ll set the alpha component of each pixel in that area to 0 (transparent).
As each single area is transparent you use compositing to draw the new image only where the existing pixels are transparent. The composite you want is context.globalCompositeOperation=”source-out”.
These examples show:
After uniquely colorizing each area.
After making 1 area transparent (the top-right area is transparent).
After compositing an image into the transparent area.
//draw some white, green, and blue stripes
for (var i = 0; i < canvas.width; i += 10) {
for (var j = 0; j < canvas.height; j += 10) {
context.fillStyle = (i % 20 === 0) ? "#fff" : ((i % 30 === 0) ? "#0f0" : "#00f");
context.fillRect(i, j, 10, 10);
}
}
var imagedata = context.getImageData(0, 0, canvas.width, canvas.height),
pixels = imagedata.data;
//if found white pixel i.e 255,255,255 changes it to 0,102,204
//you can change it to another color as u wish
for (var offset = 0, len = pixels.length; offset < len; offset += 4) {
if(pixels[offset]==255 &&
pixels[offset+1]==255 &&
pixels[offset+2]==255)
{pixels[offset] = 0; //
pixels[offset + 1] = 102; //
pixels[offset + 2] = 204; //
}
}
context.putImageData(imagedata, 0, 0);
I currently render a colour slider gradient onto a canvas element with the following code:
var colorgrad = self.cache.colorctx.createLinearGradient(0, 0, 0, self.cache.colorpicker.height);
colorgrad.addColorStop(0, 'red'); //red
colorgrad.addColorStop(1 / 6, 'yellow'); //yellow
colorgrad.addColorStop(2 / 6, 'lime') // green
colorgrad.addColorStop(3 / 6, 'aqua'); // aqua
colorgrad.addColorStop(4 / 6, 'blue'); //blue
colorgrad.addColorStop(5 / 6, 'magenta'); // pink
colorgrad.addColorStop(1, 'red'); //red
self.cache.colorctx.fillStyle = colorgrad;
self.cache.colorctx.fillRect(0, 0, 60, self.cache.colorpicker.height);
self.cache.colourdata = self.cache.colorctx.getImageData(0, 0, 1, self.cache.colorpicker.height).data;
This creates a gradient which draws all the way from through the colour wheel from red back to red and produces the image as follows:
When a user moves the circular controller at the top of the image I check the Y position of the controller against the cached image pixel data producing a simple colour picker which returns hex/RGB values.
So now that I have colour extraction working based on user input I also need to be able to take a hex colour value and work out the top value of the control. So in essence, reverse the technique. But I cannot seem to get my head around the correct algorithm to use.
I have a working hex to RGB function so the colour input can be considered an array of rgb values :
colour : {
r : 255,
g : 255,
b : 255
}
So to summarise, when knowing the pixel height of the image above, how can I calculate the y position of a given colour in this image when provided with an array of RGB values?
Thanks in advance.
You could convert the rgb color to HSV which will provide you with a color value (H) ranging from 0° (top of your slider) to 360° (bottom of your slider).
With that you can calculate the y position according to the size of the slider (height / 360 * H)