I've got a script that cycle's through images. The images start pixelated and then when they are in view, become unpixelated. I achieve that by calling this function x amount of times with requestAnimationFrame
Images.prototype.setPixels = function() {
var sw = this.imageWidth,
sh = this.imageHeight,
imageData = this.context.getImageData( 0, 0, sw, sh ),
data = imageData.data,
y, x, n, m;
for ( y = 0; y < sh; y += this.pixelation ) {
for ( x = 0; x < sw; x += this.pixelation ) {
var red = data[((sw * y) + x) * 4];
var green = data[((sw * y) + x) * 4 + 1];
var blue = data[((sw * y) + x) * 4 + 2];
for ( n = 0; n < this.pixelation; n++ ) {
for ( m = 0; m < this.pixelation; m++ ) {
if ( x + m < sw ) {
data[((sw * (y + n)) + (x + m)) * 4] = red;
data[((sw * (y + n)) + (x + m)) * 4 + 1] = green;
data[((sw * (y + n)) + (x + m)) * 4 + 2] = blue;
}
}
}
}
}
this.context.putImageData( imageData, 0, 0 );
}
Question: How can I make the individual pixels larger blocks than they are right now. Right now they are pretty small, and the effect is a little jarring. I'm hoping to fix this by having less pixel blocks on the screen, by making them bigger.
I hope this makes sense, I'm fairly green with canvas, so anything you could do to point me in the right direction would be great!
The best for this kind of effect is to simply use drawImage and let the browser handle the pixelation thanks to the nearest-neighbor anti-aliasing algorithm that can be set by changing the imageSmoothingEnabled property to false.
It then becomes a two step process to pixelate an image at any pixel_size:
draw the full quality image (or canvas / video ...) at its original size / pixel_size.
At this stage, each "pixel" is one pixel large.
draw this small image again but up-scaled by pixel_size. To do so, you just need to draw the canvas over itself.
Each pixel is now pixel_size large.
Instead of dealing with hard to read many parameters of drawImage, we can deal the scaling quite easily by just using ctx.scale() method.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function drawPixelated( source, pixel_size ) {
// scale down
ctx.scale(1 / pixel_size, 1 / pixel_size)
ctx.drawImage(source, 0, 0);
// make next drawing erase what's currently on the canvas
ctx.globalCompositeOperation = 'copy';
// nearest-neighbor
ctx.imageSmoothingEnabled = false;
// scale up
ctx.setTransform(pixel_size, 0, 0, pixel_size, 0, 0);
ctx.drawImage(canvas, 0, 0);
// reset all to defaults
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.imageSmoothingEnabled = true;
}
const img = new Image();
img.onload = animeLoop;
img.src = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
let size = 1;
let speed = 0.1;
function animeLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
size += speed;
if(size > 30 || size <= 1) {
speed *= -1
}
drawPixelated( img, size );
requestAnimationFrame(animeLoop);
}
<canvas id="canvas" width="800" height="600"></canvas>
Now for the ones that come with a real need to use an ImageData, for instance because they are generating pixel-art, then know that you can simply use the same technique:
put your ImageData with each pixel being 1 pixel large.
scale your context to pixel_size
draw your canvas over itself upscaled
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function putPixelated( imageData, pixel_size ) {
ctx.putImageData(imageData, 0, 0);
// make next drawing erase what's currently on the canvas
ctx.globalCompositeOperation = 'copy';
// nearest-neighbor
ctx.imageSmoothingEnabled = false;
// scale up
ctx.setTransform(pixel_size, 0, 0, pixel_size, 0, 0);
ctx.drawImage(canvas, 0, 0);
// reset all to defaults
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.imageSmoothingEnabled = true;
}
const img = new ImageData(16, 16);
crypto.getRandomValues(img.data);
let size = 1;
let speed = 0.1;
animeLoop();
function animeLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
size += speed;
if(size > 30 || size <= 1) {
speed *= -1
}
putPixelated( img, size );
requestAnimationFrame(animeLoop);
}
<canvas id="canvas" width="800" height="600"></canvas>
Related
I am trying to display multiple images using Canvas DrawImages with each a unique position. I have created an array with multiple images and I would like to position them on different parts of the canvas on load.
Now, all the canvas images are stacked on each other.
This is my JS:
(() => {
// Canvas Setup
let canvas = document.getElementById('hedoneCanvas');
let context = canvas.getContext('2d');
// // Canvas Images
let imageSources = [
'https://images.pexels.com/photos/1313267/pexels-photo-1313267.jpeg?cs=srgb&dl=food-fruit-green-1313267.jpg&fm=jpg',
'https://images.pexels.com/photos/2965413/pexels-photo-2965413.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2196602/pexels-photo-2196602.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2955490/pexels-photo-2955490.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'
];
// Canvas Size
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const loadImage = imagePath => {
return new Promise((resolve, reject) => {
let image = new Image();
image.addEventListener('load', () => {
resolve(image);
});
image.addEventListener('error', err => {
reject(err);
});
image.src = imagePath;
});
};
const canvasOnResize = () => {
canvas.width = window.innerWidth;
canvas.style.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.height = window.innerHeight;
};
window.addEventListener('resize', canvasOnResize);
Promise.all(imageSources.map(i => loadImage(i)))
.then(images => {
images.forEach(image => {
console.log(context);
context.drawImage(image, 0, 0, canvas.width, canvas.height);
});
})
.catch(err => {
console.error(err);
});
})();
I also want to make them responsive how can I archive this?
I have demo on Codepen: https://codepen.io/Merdzanovich/pen/dybLQqL
Trying to do something like this on hover:
http://vlnc.studio/
Responsive canvas
To make the content of a canvas responsive it is best to use a reference display size. This represents the ideal display size that your content is viewed in.
The reference is used to then calculate how to display the content on displays that do not match the ideal size.
In the example below the object reference defines the reference display and provides methods to resize the canvas and scale and position the content.
With the reference defined you can then position and size your content for the reference display.
For example the constant imageMaxSize = 512 sets the max size (width or height) of an image. The 512 is relative to the reference display (1920, 1080). The actual size that the image is display depends on the size of the page.
It sets a matrix that is used to transform the content to fit the display. Rather then use the top left of the display as origin (0,0) it sets the center of the canvas as the origin.
The example lets you set how the canvas responds to the display resolution, the const scaleMethod can be set to
"fit" will ensure that all the content will be displayed (as long as it fits the reference). However there may be blank areas above below or left and right of the content if the display aspect is different from the reference.
"fill" will ensure that the content will fill the display. However some of the content (top and bottom, or left and right) may be clipped if the display aspect does not match the reference.
Positioning images.
That just requires an array that holds the image position and size relative to the reference display.
In the example the array displayList which extends an array has the function
add(image,x,y) that adds an image to the list. The x and y represent the position of the image center and is relative to the reference display origin (center of canvas)
When an images is added its reference size is calculated from its natural size
draw(ctx) will draw all the items in the display list using the reference matrix to scale and position the images.
Rendering
Rather than render to the canvas ad-hock a render loop is used updateCanvas that ensures content is updated in sync with the display hardware. The ensure that if you have animated content it does not produce artifacts (shearing, flicker)
To prevent the rendering to needlessly draw content the render loop will only render the content when the semaphore update is set to true. For example when resizing the canvas the content needs to be rendered. This is achieved by simply setting update=true
Rather than use the resize event to resize the canvas, the render loop checks if the canvas size matches the page size. If there is a miss match then the canvas is resize. this is done because the resize event is not synced with the display hardware and will cause poor quality rendering while the display is being resized. it also ensures that the canvas is not resized more than once between display frames.
Example
requestAnimationFrame(updateCanvas);
const ctx = canvas.getContext('2d');
const SCALE_METHOD = "fit";
const images = [];
const ALPHA_FADE_IN_SPEED = 0.04; // for fade in out approx time use
// seconds = (0.016666 / ALPHA_FADE_IN_SPEED)
const FADE_OVERLAP = 0.4; // fraction of fade time. NOT less or equal to
// ALPHA_FADE_IN_SPEED and not greater equal to 0.5
const IMAGE_MAX_SIZE = 480; // image isze in pixel of reference display
const IMAGE_MIN_SIZE = IMAGE_MAX_SIZE * 0.8;
const IMAGE_SCALE_FLICK = IMAGE_MAX_SIZE * 0.05;
// sigmoid curve return val 0-1. P is power.
// 0 < p < 1 curve eases center
// 1 == p linear curve
// 1 < p curve eases out from 0 and into 1
Math.sCurve = (u, p = 2) => u <= 0 ? 0 : u >= 1 ? 1 : u ** p / (u ** p + (1 - u) ** p);
// Simple spring
// constructor(u,[a,[d,[t]]])
// u is spring position
// a is acceleration default 0.1
// d is dampening default 0.9
// t is spring target (equalibrium) default t = u
// properties
// u current spring length
// flick(v) // adds movement to spring
// step(u) gets next value of spring. target defaults to this.target
Math.freeSpring = (u, a = 0.3 , d = 0.65, t = u) => ({
u,
v : 0,
set target(v) { t = v },
flick(v) { this.v = v * (1/d) *(1/a)},
step(u = t) { return this.u += (this.v = (this.v += (u - this.u) * a) * d) }
})
var update = false;
const reference = {
get width() { return 1920 }, // ideal display resolution
get height() { return 1080 },
matrix: [1, 0, 0, 1, 0, 0],
resize(method, width = innerWidth, height = innerHeight) {
method = method.toLowerCase();
var scale = 1; // one to one of reference
if (method === "fit") {
scale = Math.min(width / reference.width, height / reference.height);
} else if (method === "fill") {
scale = Math.max(width / reference.width, height / reference.height);
}
const mat = reference.matrix;
mat[3] = mat[0] = scale;
mat[4] = width / 2;
mat[5] = height / 2;
canvas.width = width;
canvas.height = height;
update = true;
},
checkSize() {
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
reference.resize(SCALE_METHOD);
}
},
};
{
let count = 0;
[
'https://images.pexels.com/photos/1313267/pexels-photo-1313267.jpeg?cs=srgb&dl=food-fruit-green-1313267.jpg&fm=jpg',
'https://images.pexels.com/photos/2965413/pexels-photo-2965413.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2196602/pexels-photo-2196602.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
'https://images.pexels.com/photos/2955490/pexels-photo-2955490.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'
].forEach(src => {
count++;
const img = new Image;
img.src = src;
img.addEventListener("load", () => {
images.push(img);
if (! --count) { setup() }
})
img.addEventListener("error", () => {if (! --count) { setup() }});
});
}
const displayList = Object.assign([], {
add(image, x, y) {
var item;
var w = image.naturalWidth;
var h = image.naturalHeight;
const scale = Math.min(IMAGE_MAX_SIZE / w, IMAGE_MAX_SIZE / h);
w *= scale;
h *= scale;
displayList.push(item = {
image, x, y, w, h,
fading: false,
alpha: 0,
alphaStep: 0,
onAlphaReady: undefined,
scaleFX: Math.freeSpring(IMAGE_MIN_SIZE)
});
displayList.fadeQueue.push(item);
return item;
},
fadeQueue: [],
draw(ctx) {
var curvePower = 2
ctx.setTransform(...reference.matrix);
for (const item of displayList) {
if (item.fading) {
item.alpha += item.alphaStep;
curvePower = item.alphaStep > 0 ? 2 : 2;
if (item.onAlphaReady && (
(item.alphaStep < 0 && item.alpha <= FADE_OVERLAP) ||
(item.alphaStep > 0 && item.alpha >= 1 - FADE_OVERLAP))) {
item.onAlphaReady(item);
item.onAlphaReady = undefined;
} else if (item.alpha <= 0 || item.alpha >= 1) {
item.fading = false;
}
update = true;
}
ctx.globalAlpha = Math.sCurve(item.alpha, curvePower);
const s = item.scaleFX.step() / IMAGE_MAX_SIZE;
ctx.drawImage(item.image, item.x - item.w / 2 * s, item.y - item.h / 2 * s, item.w * s, item.h * s);
}
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0); // default transform
}
});
function fadeNextImage() {
const next = displayList.fadeQueue.shift();
if(next.alpha < 0.5) { // Start fade in
next.scaleFX.flick(IMAGE_SCALE_FLICK);
next.scaleFX.target = IMAGE_MAX_SIZE;
next.alphaStep = ALPHA_FADE_IN_SPEED;
} else { // Start fade out
next.scaleFX.flick(IMAGE_SCALE_FLICK);
next.scaleFX.target = IMAGE_MIN_SIZE;
next.alphaStep = -ALPHA_FADE_IN_SPEED;
}
next.onAlphaReady = fadeNextImage;
next.fading = true;
displayList.fadeQueue.push(next);
}
function setup() {
const repeat = 2;
var i, len = images.length;
const distX = (reference.width - IMAGE_MAX_SIZE) * 0.45;
const distY = (reference.height - IMAGE_MAX_SIZE) * 0.45;
for (i = 0; i < len * repeat; i++) {
const ang = i / (len * repeat) * Math.PI * 2 - Math.PI / 2;
displayList.add(images[i % len], Math.cos(ang) * distX, Math.sin(ang) * distY);
}
fadeNextImage();
}
function clearCanvas() {
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function loading(time) {
clearCanvas();
ctx.font = "12px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.strokeStyle = "#aaa";
ctx.fillStyle = "white";
ctx.setTransform(1,0,0,1,ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.fillText("loading",0,0);
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = "round";
const pos = time + Math.cos(time) * 0.25 + 1;
ctx.arc(0 ,0, 24, pos, pos + Math.cos(time * 0.1) * 0.5 + 1);
ctx.stroke();
}
function updateCanvas(time) {
reference.checkSize()
if(!displayList.length) {
loading(time / 100);
} else if (update) {
update = false;
clearCanvas();
displayList.draw(ctx);
}
requestAnimationFrame(updateCanvas);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
background: black;
}
<canvas id="canvas"></canvas>
Try this:
Promise.all(imageSources.map(i => loadImage(i)))
.then(images => {
images.forEach((image,key) => {
context.drawImage(image, key*100, key*100, canvas.width, canvas.height);
});
})
.catch(err => {
console.error(err);
});
I am trying to achieve a tracing effect where the lines have a faded trail. The way I am trying to do it is simply by drawing the solid background once, and then on further frames draw a transparent background before drawing the new lines, so that you can still see a little of the image before it.
The issue is that I do want the lines to fade out completely after some time, but they seem to leave a permanent after image, even after drawing over them repeatedly.
I've tried setting different globalCompositeOperation(s) and it seemed like I was barking up the wrong tree there.
This code is called once
//initiate trace bg
traceBuffer.getContext("2d").fillStyle = "rgba(0, 30, 50, 1)";
traceBuffer.getContext("2d").fillRect(0, 0, traceBuffer.width, traceBuffer.height);
then inside the setInterval function it calls
//draw transparent background
ctx.fillStyle = "rgba(0, 30, 50, 0.04)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//set drawing settings
ctx.strokeStyle = "#AAAAAA";
ctx.lineWidth = 4;
for (let i = 0; i < tracer.layer2.length; i++){
ctx.beginPath();
ctx.moveTo(newX, newY);
ctx.lineTo(oldX, oldY);
ctx.stroke();
}
Here's an example: https://i.imgur.com/QTkeIVf.png
On the left is what I am currently getting, and on the right is the edit of what I actually want to happen.
This is how I would do it. I would build a history of the particles moving on the track. The older the position the smaller the value of the alpha value for the fill. Also for a nicer effect I would reduce the size of the circle.
I hope this is what you need.
PS: I would have loved to have your curve. Since I don't have it I've drawn a different one.
const hypotrochoid = document.getElementById("hypotrochoid");
const ctx = hypotrochoid.getContext("2d");
const cw = (hypotrochoid.width = 300);
const ch = (hypotrochoid.height = 300);
const cx = cw / 2,
cy = ch / 2;
ctx.lineWidth = 1;
ctx.strokeStyle = "#d9d9d9";
// variables for the hypotrochoid
let a = 90;
let b = 15;
let h = 50;
// an array where to save the points used to draw the track
let track = [];
//add points to the track array. This will be used to draw the track for the particles
for (var t = 0; t < 2 * Math.PI; t += 0.01) {
let o = {};
o.x = cx + (a - b) * Math.cos(t) + h * Math.cos((a - b) / b * t);
o.y = cy + (a - b) * Math.sin(t) - h * Math.sin((a - b) / b * t);
track.push(o);
}
// a function to draw the track
function drawTrack(ry) {
ctx.beginPath();
ctx.moveTo(ry[0].x, ry[0].y);
for (let t = 1; t < ry.length; t++) {
ctx.lineTo(ry[t].x, ry[t].y);
}
ctx.closePath();
ctx.stroke();
}
// a class of points that are moving on the track
class Point {
constructor(pos) {
this.pos = pos;
this.r = 3;//the radius of the circle
this.history = [];
this.historyLength = 40;
}
update(newPos) {
let old_pos = {};
old_pos.x = this.pos.x;
old_pos.y = this.pos.y;
//save the old position in the history array
this.history.push(old_pos);
//if the length of the track is longer than the max length allowed remove the extra elements
if (this.history.length > this.historyLength) {
this.history.shift();
}
//gry the new position on the track
this.pos = newPos;
}
draw() {
for (let i = 0; i < this.history.length; i++) {
//calculate the alpha value for every element on the history array
let alp = i * 1 / this.history.length;
// set the fill style
ctx.fillStyle = `rgba(0,0,0,${alp})`;
//draw an arc
ctx.beginPath();
ctx.arc(
this.history[i].x,
this.history[i].y,
this.r * alp,
0,
2 * Math.PI
);
ctx.fill();
}
}
}
// 2 points on the track
let p = new Point(track[0]);
let p1 = new Point(track[~~(track.length / 2)]);
let frames = 0;
let n, n1;
function Draw() {
requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
//indexes for the track position
n = frames % track.length;
n1 = (~~(track.length / 2) + frames) % track.length;
//draw the track
drawTrack(track);
// update and draw the first point
p.update(track[n]);
p.draw();
// update and draw the second point
p1.update(track[n1]);
p1.draw();
//increase the frames counter
frames++;
}
Draw();
canvas{border:1px solid}
<canvas id="hypotrochoid"></canvas>
I want to create a random generated image (random colors), like this one. But, I want to do it in javascript, but for some reason I am getting black screen.
Here is my code:
var g=document . createElement( 'canvas').getContext('2d');
g.canvas.width=g.canvas.height = 800;
g.imgd = g.getImageData(0, 0, 800, 800);
g.data = g.imgd.data;
g.data.forEach((_, index) => (index & 3) < 3 && (g.data[index] = Math.random()));
g.putImageData(g.imgd, 0, 0);
document.body.appendChild(g.canvas);;;
And i am getting black screen, and on some websites it is white screen. So what is what not working in my script? My english is not very good, but can someone explain what is wrong, my code dont'esnt working.
I also tried different dimensions of canvas and I dont see any errors so what is wrong?
You are using Math.random() which generates floats from 0 to 1 without including 1. Since you're applying zeroes to the color components (the data from getImageData().data), you get the color black (rgb(0, 0, 0)).
Here's a more readable solution:
var canvas = document.createElement('canvas');
canvas.width = canvas.height = 800;
var ctx = canvas.getContext('2d');
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
for (var i = 0; i < imgData.data.length; i += 4) {
imgData.data[i] = randomInt(0, 255); // red
imgData.data[i+1] = randomInt(0, 255); // green
imgData.data[i+2] = randomInt(0, 255); // blue
imgData.data[i+3] = 255; // alpha
}
ctx.putImageData(imgData, 0, 0);
document.body.appendChild(canvas);
Math.random() returns a floating point number, not within the full range of 0-255. You can alternatively use .fillStyle() and set the color to a random hex color.
function pixels(width = 100, height = 100, size = 1, canvas) {
var canvas = canvas || document.createElement("canvas");
var ctx = canvas.getContext("2d");
var total = [];
canvas.width = width;
canvas.height = height;
function random() {
return "XXXXXX".replace(/X/g, function() {
var seed = "a0b1c2d3e4f56789";
return seed.charAt(Math.floor(Math.random() * seed.length))
})
};
for (var x = 0; x <= width; x += size) {
total.push(x)
};
total.forEach(function(value, index) {
for (var i = 0; i <= height; i++) {
ctx.fillStyle = "#" + random();
ctx.fillRect(value, total[i], size, size);
}
});
document.body.appendChild(canvas);
return ctx;
};
var c = pixels(window.innerWidth - 20, window.innerHeight - 20);
I found this script for converting an image to black and white, which works great, but I was hoping to understand the code a little bit better. I put my questions in the code, in the form of comments.
Can anyone explain in a little more detail what is happening here:
function grayscale(src){ //Creates a canvas element with a grayscale version of the color image
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var imgObj = new Image();
imgObj.src = src;
canvas.width = imgObj.width;
canvas.height = imgObj.height;
ctx.drawImage(imgObj, 0, 0); //Are these CTX functions documented somewhere where I can see what parameters they require / what those parameters mean?
var imgPixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
for(var y = 0; y < imgPixels.height; y++){
for(var x = 0; x < imgPixels.width; x++){
var i = (y * 4) * imgPixels.width + x * 4; //Why is this multiplied by 4?
var avg = (imgPixels.data[i] + imgPixels.data[i + 1] + imgPixels.data[i + 2]) / 3; //Is this getting the average of the values of each channel R G and B, and converting them to BW(?)
imgPixels.data[i] = avg;
imgPixels.data[i + 1] = avg;
imgPixels.data[i + 2] = avg;
}
}
ctx.putImageData(imgPixels, 0, 0, 0, 0, imgPixels.width, imgPixels.height);
return canvas.toDataURL();
}
The canvas functions are, like most functions, described in an official specification. Also, MDC is helpful for more "informal" articles. E.g. the drawImage function on MDC is here.
The getImageData function returns an object, which contains an array with the byte data of all pixels. Each pixel is described by 4 bytes: r, g, b and a.
r, g and b are the color components (red, green and blue) and alpha is the opacity. So each pixel uses 4 bytes, and therefore a pixel's data begins at pixel_index * 4.
Yes, it's averaging the values. Because in the next 3 lines r, g and b are all set to that same value, you'll obtain a gray color for each pixel (because the amount of all 3 components are the same).
So basically, for all pixels this will hold: r === g, g === b and thus also r === b. Colors for which this holds are grayscale (0, 0, 0 being black and 255, 255, 255 being white).
function grayscale(src){ //Creates a canvas element with a grayscale version of the color image
//create canvas
var canvas = document.createElement('canvas');
//get its context
var ctx = canvas.getContext('2d');
//create empty image
var imgObj = new Image();
//start to load image from src url
imgObj.src = src;
//resize canvas up to size image size
canvas.width = imgObj.width;
canvas.height = imgObj.height;
//draw image on canvas, full canvas API is described here http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html
ctx.drawImage(imgObj, 0, 0);
//get array of image pixels
var imgPixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
//run through all the pixels
for(var y = 0; y < imgPixels.height; y++){
for(var x = 0; x < imgPixels.width; x++){
//here is x and y are multiplied by 4 because every pixel is four bytes: red, green, blue, alpha
var i = (y * 4) * imgPixels.width + x * 4; //Why is this multiplied by 4?
//compute average value for colors, this will convert it to bw
var avg = (imgPixels.data[i] + imgPixels.data[i + 1] + imgPixels.data[i + 2]) / 3;
//set values to array
imgPixels.data[i] = avg;
imgPixels.data[i + 1] = avg;
imgPixels.data[i + 2] = avg;
}
}
//draw pixels according to computed colors
ctx.putImageData(imgPixels, 0, 0, 0, 0, imgPixels.width, imgPixels.height);
return canvas.toDataURL();
}
In this function coefficient equal to 1/3 are used, however the usually used are: 0.3R + 0.59G + 0.11B (http://gimp-savvy.com/BOOK/index.html?node54.html).
I have a static image (a png) that I'm drawing on a canvas element using drawImage.
var _canvas = null;
var _context = null;
var _curImageData;
function copyImageToCanvas(aImg)
{
_canvas = document.getElementById("canvas");
var w = aImg.naturalWidth;
var h = aImg.naturalHeight;
_canvas.width = w;
_canvas.height = h;
_context = _canvas.getContext("2d");
_context.clearRect(0, 0, w, h);
_context.drawImage(aImg, 0, 0);
_curImageData = _context.getImageData(0, 0, w, h);
}
I then manipulate the image data pixel by pixel (setting the opacity to 255 or 0 depending on the pixel value) and then update the canvas using putImageData.
function updateImage(maxPixelValToShow)
{
for (var x = 0; x < _curImageData.width; x++)
for (var y = 0; y < _curImageData.height; y++)
{
var offset = (y * _curImageData.width + x) * 4;
var r = _curImageData.data[offset];
var g = _curImageData.data[offset + 1];
var b = _curImageData.data[offset + 2];
var a = _curImageData.data[offset + 3];
var pixelNum = ((g * 255) + b);
//if greater than max or black/no data
if (pixelNum > maxPixelValToShow || (!r && !g && !b)) {
_curImageData.data[offset + 3] = 0;
} else {
_curImageData.data[offset + 3] = 255;
}
}
_context.putImageData(_curImageData, 0, 0);
}
And this all works perfectly. The issue I'm having is when I create a canvas twice the size of my image (or half the size, for that matter) and draw the image to fit the canvas size.
function copyImageToCanvas(aImg)
{
_canvas = document.getElementById("canvas");
var w = aImg.naturalWidth * 2; //double the size!
var h = aImg.naturalHeight * 2; //double the size!
_canvas.width = w;
_canvas.height = h;
_context = _canvas.getContext("2d");
_context.clearRect(0, 0, w, h);
_context.drawImage(aImg, 0, 0, w, h);
_curImageData = _context.getImageData(0, 0, w, h);
}
I'm getting lots of strange artifacts around the edges of my image when I call my updateImage function. Does anyone know a) why this is happening and b) what can I do about it?
Here are some images to show what is being generated:
The original image
The original image after my updateImage function call
The resized image
The resized image after my updateImage function call
[Solution]
Thanks Adam and sunetos, that was the problem. It worked fine once I setup one canvas to store the original file data and do the pixel manipulation and another only for display. Code snippet below:
function updateImage(maxPixelValToShow) {
//manipulate the _workingContext data
_workingContext.putImageData(_curImageData, 0, 0);
_displayContext.clearRect(0, 0, _workingCanvas.width*_scale, _workingCanvas.height*_scale);
_displayContext.drawImage(_workingCanvas, 0, 0, _workingCanvas.width*_scale, _workingCanvas.height*_scale);
}
It looks like the image smoothing used by the browser when scaling up the image in drawImage() antialiased the edges of your shape, causing new intermediate color values around the edge that are not handled by your updateImage() function. Like Adam commented, you should apply your updateImage() logic against the unmodified original pixels, and then scale it.