Manipulate an image on a canvas - javascript

I have to load an image on a canvas object and after clicking some buttons or sliding some sliders, I need to change some pixels on this canvas. Thats my code so far:
function CanvasImage(element, src) {
var width, height;
var canvas = document.getElementById(element);
var ctx = canvas.getContext("2d");
var img = new Image;
img.onload = function () {
width = canvas.width = img.width;
height = canvas.height = img.width;
ctx.drawImage(img, 0, 0, width, height)
};
img.src = src;
this.brightness = function (amount) {
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imgData.data;
for (var i = 0; i < data.length; i += 4) {
data[i] += amount;
data[i + 1] += amount;
data[i + 2] += amount
}
imgData.data = data;
ctx.putImageData(imgData, 0, 0)
};
this.transform = function (data) {
ctx.save();
this.brightness(data.brightness);
ctx.restore()
}
}
Thats I've just added the function to manipulate the brightness of the image. At the beginning, the image is loaded to the canvas element. That works fine for me. The function brightness is just adding the giving amount to the pixels. That also works. I think my problem is the transform function. I save the state of the canvas context and call my transform function. After this manipulation I restore the state. I would say, the context should look like the earlier loaded image.
My problem now is, that after changing a slider and returning it to the original state my image is still brighter or darker and doesn't have the original state of the image. Do you have any suggestions for me, please?
I appreciate every answer. :D

save and restore only saves the state of the canvas not image data. State are things such as styles, translations, text alignment and so forth.
In order to get back original content you need to either redraw the source or store the data to be restored with another canvas element (ie. off-screen canvas), or store the data as an array (getImageData()).
Also worth to notice is that putImageData by-passes the canvas' state and changes pixels directly independently.
My recommendation would be to keep the source available one way or another and always redraw it to canvas before a new alteration takes place. For reset simply redraw source and skip alteration.
ONLINE DEMO HERE
this.brightness = function (amount) {
ctx.drawImage(img, 0, 0, width, height)
/// no change, just exit
if (amount === 0) return;
/// ... snipped
};

Related

How to crop color portion from image canvas?

I am working on ionic based application.
I want to functionality like user has Filled the red color on image(canvas) with finger. So I have done the filled functionality but I want to crop the Filled portion from canvas. I have attached one image for reference.
I want to crop red portion from above image. I has googling but not found any solution.
Creating a image mask.
If you are rendering the selection area (red) then the solution is simple.
Create a second canvas the same size as the drawing, and don't add it to the DOM. Draw the red marking content onto that canvas
The on the display canvas render the image first and then render that marking
canvas over the image with composite mode like "overlay" so that the original image can be seen and the marked areas are red.
Now you have two layers, one is the image and the other the mask you can use to get a copy of the marked content.
To do that create a 3rd canvas, draw the original image onto it, then set the composite mode to "destination-in". Then draw the mask over it. Only the marked pixels will remain.
See the example for more details
setTimeout(example,0); // ensures that the run us after parsing
function example(){
const ctx = canvas.getContext("2d");
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var selectLayer = CImageCtx(w,h); // creates a canvas
var selectedContent = CImageCtx(w,h); // the selected content
document.body.appendChild(selectedContent);
var image = new Image; // the image
image.src = " https://i.stack.imgur.com/QhFct.png";
// updates the masked result
function updateSelected(){
var ctx = selectedContent.ctx;
ctx.drawImage(image,0,0);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(selectLayer,0,0);
ctx.globalCompositeOperation = "source-over";
}
function update(){
// if mouse down then
if(mouse.but){
// clear the mask if on the right image
if(mouse.oldBut === false && mouse.x > 256){
selectLayer.ctx.clearRect(0,0,w,h);
mouse.but = false;
}else{
// draw the red
selectLayer.ctx.fillStyle = "red";
fillCircle(mouse.x, mouse.y, 20, selectLayer.ctx);
}
// update the masked result
updateSelected();
}
// clear the canvas
ctx.clearRect(0,0,w,h);
// draw the image
ctx.drawImage(image,0,0);
// then draw the marking layer over it with comp overlay
ctx.globalCompositeOperation = "overlay";
ctx.drawImage(selectLayer,0,0);
ctx.globalCompositeOperation = "source-over";
mouse.oldBut = mouse.but;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
//#############################################################################
// helper functions not part of the answer
//#############################################################################
const mouse = {
x : 0, y : 0, but : false,
events(e){
const m = mouse;
const bounds = canvas.getBoundingClientRect();
m.x = e.pageX - bounds.left - scrollX;
m.y = e.pageY - bounds.top - scrollY;
m.but = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.but;
}
};
(["down","up","move"]).forEach(name => document.addEventListener("mouse" + name,mouse.events));
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
const fillCircle = (l,y=ctx,r=ctx,c=ctx) =>{if(l.p1){c=y; r=leng(l);y=l.p1.y;l=l.p1.x }else if(l.x){c=r;r=y;y=l.y;l=l.x}c.beginPath(); c.arc(l,y,r,0,Math.PI*2); c.fill()}
body { font-family : arial; }
canvas { border : 2px solid black; }
Draw on image and the selected parts are shown on the right<br>
Click right image to reset selection<br>
<canvas id="canvas" width=256 height=256></canvas>
Already masked.
If the red mask is already applied to the image then there is not much you can do apart from do a threshold filter depending on how red the image is. But even then you are going to have problems with darker areas, and areas that already contain red.
Unless you have the original image you will have poor results.
If you have the original image then you will have to access the image data and create a new image as a mask by comparing each pixel and selecting only pixels that are different. That will force you to same domain images only (or with CORS cross origin headers)

Change html canvas black background to white background when creating jpg image from png image

I have a canvas which is loaded with a png image. I get its jpg base64 string by .toDataURL() method like this:
$('#base64str').val(canvas.toDataURL("image/jpeg"));
But the transparent parts of the png image are shown black in the new jpg image.
Any solutions to change this color to white? Thanks in advance.
This blackening occurs because the 'image/jpeg' conversion involves setting the alpha of all canvas pixels to fully opaque (alpha=255). The problem is that transparent canvas pixels are colored fully-black-but-transparent. So when you turn these black pixels opaque, the result is a blackened jpeg.
The workaround is to manually change all non-opaque canvas pixels to your desired white color instead of black.
That way when they are made opaque they will appear as white instead of black pixels.
Here's how:
// change non-opaque pixels to white
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
var data=imgData.data;
for(var i=0;i<data.length;i+=4){
if(data[i+3]<255){
data[i]=255;
data[i+1]=255;
data[i+2]=255;
data[i+3]=255;
}
}
ctx.putImageData(imgData,0,0);
After spending a lot of time on this and this post specifically, and these solutions kinda worked expect I just couldn't get the canvas to look right. Anyway I found this solution elsewhere and wanted to post it here incase it helps someone else from spending hours trying to get the black background to white and look like the original.
public getURI(): string {
let canvas = <HTMLCanvasElement>document.getElementById('chartcanvas');
var newCanvas = <HTMLCanvasElement>canvas.cloneNode(true);
var ctx = newCanvas.getContext('2d');
ctx.fillStyle = "#FFF";
ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
ctx.drawImage(canvas, 0, 0);
return newCanvas.toDataURL("image/jpeg");
}
This answer is a bit longer, but I find it to be more 'correct' in that it handles these things without directly modifying raw canvas data. I find that to be a pretty messy and theoretically unsatisfying solution. There are built in functions to achieve that, and they ought to be used. Here is the solution I found/pilfered:
function canvasToImage(backgroundColor){
var context = document.getElementById('canvas').getContext('2d');
canvas = context.canvas;
//cache height and width
var w = canvas.width;
var h = canvas.height;
var data;
//get the current ImageData for the canvas.
data = context.getImageData(0, 0, w, h);
//store the current globalCompositeOperation
var compositeOperation = context.globalCompositeOperation;
//set to draw behind current content
context.globalCompositeOperation = "destination-over";
//set background color
context.fillStyle = backgroundColor;
//draw background / rect on entire canvas
context.fillRect(0,0,w,h);
//get the image data from the canvas
var imageData = this.canvas.toDataURL("image/jpeg");
//clear the canvas
context.clearRect (0,0,w,h);
//restore it with original / cached ImageData
context.putImageData(data, 0,0);
//reset the globalCompositeOperation to what it was
context.globalCompositeOperation = compositeOperation;
//return the Base64 encoded data url string
return imageData;
}
Basically, you create a white background image and underlay it under the canvas and then print that. This function is mostly plagiarized from someone's blog, but it required a bit of modification -- such as actually getting the context -- and copied directly from my (working) code, so as long as your canvas element has the id 'canvas', you should be able to copy/paste it and have it work.
This is the blog post I modified it from:
http://www.mikechambers.com/blog/2011/01/31/setting-the-background-color-when-generating-images-from-canvas-todataurl/
The big advantage of my function over this is that it outputs to jpeg instead of png, which is more likely to work well in chrome, which has a dataurl limit of 2MB, and it actually grabs the context, which was a glaring omission in the original function.
Marks answer is correct, but when a picture has some antialiasing applied, the exported image won't be as good as it should be (mainly text). I would like to enhance his solution:
// change non-opaque pixels to white
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
var data=imgData.data;
for(var i=0;i<data.length;i+=4){
if(data[i+3]<255){
data[i] = 255 - data[i];
data[i+1] = 255 - data[i+1];
data[i+2] = 255 - data[i+2];
data[i+3] = 255 - data[i+3];
}
}
ctx.putImageData(imgData,0,0);
If you want to move to white only full transparent pixels just check for (data[i+3]==0) instead of (data[i+3]<255).
Why not to save it as PNG?
canvas.toDataURL("image/png");
// change non-opaque pixels to white
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
var data=imgData.data;
for(var i=0;i<data.length;i+=4){
if(data[i+3]<255){
data[i] = 255 - data[i];
data[i+1] = 255 - data[i+1];
data[i+2] = 255 - data[i+2];
data[i+3] = 255 - data[i+3];
}
Here is my function that resizes a photo and handles the black transparent background problem:
resizeImage({ file, maxSize, backgroundColor }) {
const fr = new FileReader();
const img = new Image();
const dataURItoBlob = (dataURI) => {
const bytes = (dataURI.split(',')[0].indexOf('base64') >= 0)
? window.atob(dataURI.split(',')[1])
: window.unescape(dataURI.split(',')[1]);
const mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
const max = bytes.length;
const ia = new Uint8Array(max);
for (let i = 0; i < max; i += 1) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ia], { type: mime });
};
const resize = () => {
// create a canvas element to manipulate
const canvas = document.createElement('canvas');
canvas.setAttribute('id', 'canvas');
const context = canvas.getContext('2d');
// setup some resizing definitions
let { width, height } = img;
const isTooWide = ((width > height) && (width > maxSize));
const isTooTall = (height > maxSize);
// resize according to `maxSize`
if (isTooWide) {
height *= maxSize / width;
width = maxSize;
} else if (isTooTall) {
width *= maxSize / height;
height = maxSize;
}
// resize the canvas
canvas.width = width;
canvas.height = height;
// place the image on the canvas
context.drawImage(img, 0, 0, width, height);
// get the current ImageData for the canvas
const data = context.getImageData(0, 0, width, height);
// store the current globalCompositeOperation
const compositeOperation = context.globalCompositeOperation;
// set to draw behind current content
context.globalCompositeOperation = 'destination-over';
// set background color
context.fillStyle = backgroundColor;
// draw background / rect on entire canvas
context.fillRect(0, 0, width, height);
// get the image data from the canvas
const imageData = canvas.toDataURL('image/jpeg');
// clear the canvas
context.clearRect(0, 0, width, height);
// restore it with original / cached ImageData
context.putImageData(data, 0, 0);
// reset the globalCompositeOperation to what it was
context.globalCompositeOperation = compositeOperation;
// return the base64-encoded data url string
return dataURItoBlob(imageData);
};
return new Promise((resolve, reject) => {
if (!file.type.match(/image.*/)) {
reject(new Error('VImageInput# Problem resizing image: file must be an image.'));
}
fr.onload = (readerEvent) => {
img.onload = () => resolve(resize());
img.src = readerEvent.target.result;
};
fr.readAsDataURL(file);
});
},
That is a Vue JS instance method that can be used like this:
// this would be the user-uploaded file from the input element
const image = file;
const settings = {
file: image,
maxSize: 192, // to make 192x192 image
backgroundColor: '#FFF',
};
// this will output a base64 string you can dump into your database
const resizedImage = await this.resizeImage(settings);
My solution here is a combination of about 74 different StackOverflow answers related to resizing images client-side, and the final boss was to handle transparent PNG files.
My answer would not be possible without Laereom's answer here.

How to step back in HTML5 Canvas history

I have an image cropper using Imgly HTML5 Canvas plugin. I need to be able to setup a history stack for the cropper to be able to undo a crop operation. Currently, I can clear the canvas on button click, but I need to be able to retain the original image, and just move back through a history of changes of the image in the canvas, in the event that a cropping step is done incorrectly.
I have the following which simply clears the canvas:
$("#renderButton").click(function() {
var elem = $(".imgly-canvas");
var canvas = elem.get(0);
var context = canvas.getContext("2d");
$('#file').val('');
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
});
The plugin creates the canvas element on image load with:
Utils.getImageDataForImage = function(image) {
var canvas, context;
canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
return context.getImageData(0, 0, image.width, image.height);
};
And this is used on resize:
Utils.cloneImageData = function(imageData) {
var i, newImageData, _i, _ref;
newImageData = this.sharedContext.createImageData(imageData.width, imageData.height);
for (i = _i = 0, _ref = imageData.data.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
newImageData.data[i] = imageData.data[i];
}
return newImageData;
};
/*
#param {Object} dimensions
#param {Integer} dimensions.width
#param {Integer} dimensions.height
#returns {HTMLCanvasElement}
*/
Utils.newCanvasWithDimensions = function(dimensions) {
var canvas;
canvas = document.createElement("canvas");
canvas.width = dimensions.width;
canvas.height = dimensions.height;
return canvas;
};
/*
#param {imageData} imageData
#returns {HTMLCanvasElement}
*/
Utils.newCanvasFromImageData = function(imageData) {
var canvas, context;
canvas = document.createElement("canvas");
canvas.width = imageData.width;
canvas.height = imageData.height;
context = canvas.getContext("2d");
context.putImageData(imageData, 0, 0);
return canvas;
};
So I'm not sure how to build a call stack to reference each change and move back through a history of modifications to an image in the canvas.
The HTML5 canvas nicely converts to JSON which can then be used to reload the canvas. You can store this in a global object.
var myObj = window.myObj || {};
myObj = {
history: [],
canvas: null
};
Get the canvas data:
myObj.canvas = document.getElementById('canvas-id');
var ctx = myObj.canvas.getContext('2d');
var data = JSON.stringify(ctx.getImageData(0, 0, myObj.canvas.width, myObj.canvas.height));
myObj.history.push(data);
Reload data:
var reloadData = JSON.parse(myObj.history[someIndex]);
var ctx = myObj.canvas.getContext('2d');
ctx.putImageData(reloadData, 0, 0);
Once you can store/load data the tricky part is managing the myObj.history array.
You should look at the command pattern. Basically, you need to write a function for every action that the user can do. When they click a button or load an image, don't call the function right away. Instead, create a command object with all the information needed to execute the command plus the information needed to undo it.
Commands are applied to a data model (the image and the crop marks). A command "load image" needs to record the new and the previous image URL so you can load the correct image when you move through the history.
For crop commands, you need to store the old and new crop rectangles - if you keep a copy of the original image around. When the command is executed, you apply the new crop rectangle on the original and draw that on the canvas.
For undo, you use the original image and the previous crop rectangle.
So the trick is to define a data model which contains all the information how the UI looks like (which is often hard to get from the UI directly - you can't get crop information after rendering the cropped image to the canvas). The commands then manipulate this state (so the next command can save it for undo) and update the UI.

Canvas variable not updating when link is clicked

See link: http://jndgn.com/colortheory/bezold.html
When clicking the 'Bigger Stripes' link in the nav I'm attempting to simply update the number of colored stripes in the canvas below it. The variable 'stripeNum' is set at 100 and when clicked should change to 20 via:
$(function(){
$("#stripes").on("click",function(e){
e.preventDefault();
if(stripeNum == 100) {
stripeNum = 20;
} else {
stripeNum = 100;
}
});
})
...but when clicked, no action occurs. Do I somehow need to get the canvas to update itself? A bit of an HTML5 rookie here. Thanks in advance for any thoughts.
JSFiddle attached for easier code transparency.
You need to re-draw the canvas, as well as clearing it when you do this. You could simply make this adjustment and you'd be good:
First, wrap the canvas drawing you have in a function:
var contextFill = function() {
context.clearRect(0, 0, canvas.width, canvas.height); // Clear on draw
for (var i = 0; i < stripeNum + 1; i++) {
context.beginPath();
context.rect(0, i * canvas.height / stripeNum, canvas.width, canvas.height / (2 * stripeNum));
context.fillStyle = randomColor;
context.fill();
}
}
Then, call contextFill() on document ready. The only addition I made to the above is context.clearRect, to clear the canvas.
Finally, also call it when your bigger stripes is called, e.g.:
...
if (stripeNum == 100) {
stripeNum = 20;
} else {
stripeNum = 100;
}
contextFill();
If you want to update canvas on window resize, you can watch the resize event on the window. The downside is it can occur many times depending on how the user is resizing. You can use a library like lodash to ensure that the function only is called every N milliseconds. For example:
window.addEventListener('resize', _.debounce(function() {
contextFill();
}, 500));

HTML canvas - drawing disappear on resizing

I have created a basic shape in HTML canvas element which works fine.
The problem occurs when I resize the canvas, all the drawing in the canvas disappears. Is this the normal behavior? or is there a function that can be used to stop this?
One way to fix this could be to call drawing function again on canvas resize however this may not be very efficient if there is huge content to be drawn.
What's the best way?
Here is the link to sample code https://gist.github.com/2983915
You need to redraw the scene when you resize.
setting the width or height of a canvas, even if you are setting it to the same value as before, not only clears the canvas but resets the entire canvas context. Any set properties (fillStyle, lineWidth, the clipping region, etc) will also be reset.
If you do not have the ability to redraw the scene from whatever data structures you might have representing the canvas, you can always save the entire canvas itself by drawing it to an in-memory canvas, setting the original width, and drawing the in-memory canvas back to the original canvas.
Here's a really quick example of saving the canvas bitmap and putting it back after a resize:
http://jsfiddle.net/simonsarris/weMbr/
Everytime you resize the canvas it will reset itself to transparant black, as defined in the spec.
You will either have to:
redraw when you resize the canvas, or,
don't resize the canvas
One another way is to use the debounce if you are concerned with the performance.
It doesnt resize or redraw every position you are dragging. But it will resize only when the it is resized.
// Assume canvas is in scope
addEventListener.("resize", debouncedResize );
// debounce timeout handle
var debounceTimeoutHandle;
// The debounce time in ms (1/1000th second)
const DEBOUNCE_TIME = 100;
// Resize function
function debouncedResize () {
clearTimeout(debounceTimeoutHandle); // Clears any pending debounce events
// Schedule a canvas resize
debounceTimeoutHandle = setTimeout(resizeCanvas, DEBOUNCE_TIME);
}
// canvas resize function
function resizeCanvas () { ... resize and redraw ... }
I had the same problem. Try following code
var wrapper = document.getElementById("signature-pad");
var canvas = wrapper.querySelector("canvas");
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
It keeps the drawing as it is
One thing that worked for me was to use requestAnimationFrame().
let height = window.innerHeight;
let width = window.innerWidth;
function handleWindowResize() {
height = window.innerHeight;
width = window.innerWidth;
}
function render() {
// Draw your fun shapes here
// ...
// Keep this on the bottom
requestAnimationFrame(render);
}
// Canvas being defined at the top of the file.
function init() {
ctx = canvas.getContext("2d");
render();
}
I had the same problem when I had to resize the canvas to adjust it to the screen.
But I solved it with this code:
var c = document.getElementById('canvas');
ctx = c.getContext('2d');
ctx.fillRect(0,0,20,20);
// Save canvas settings
ctx.save();
// Save canvas context
var dataURL = c.toDataURL('image/jpeg');
// Resize canvas
c.width = 50;
c.height = 50;
// Restore canvas context
var img = document.createElement('img');
img.src = dataURL;
img.onload=function(){
ctx.drawImage(img,20,20);
}
// Restote canvas settings
ctx.restore();
<canvas id=canvas width=40 height=40></canvas>
I also met this problem.but after a experiment, I found that Resizing the canvas element will automatically clear all drawings off the canvas!
just try the code below
<canvas id = 'canvas'></canvas>
<script>
var canvas1 = document.getElementById('canvas')
console.log('canvas size',canvas1.width, canvas1.height)
var ctx = canvas1.getContext('2d')
ctx.font = 'Bold 48px Arial'
var f = ctx.font
canvas1.width = 480
var f1 = ctx.font
alert(f === f1) //false
</script>

Categories

Resources