Related
I was trying to find the way to rotate ctx around Y-axis only using Javascript not CSS.
My code is below.
function renderFrame() {
requestAnimationFrame(renderFrame);
x = 0;
analyser.getByteFrequencyData(dataArray);
ctx1.fillStyle = "rgba(0,0,0,0.2)";
ctx1.fillRect(0, 0, WIDTH, HEIGHT);
ctx2.fillStyle = "rgba(0,0,0,0.2)";
ctx2.fillRect(0, 0, WIDTH, HEIGHT);
let r, g, b;
let bars = 118;
for (let i = 0; i < bars; i++) {
barHeight = dataArray[i] * 2.5;
if (dataArray[i] > 210) {
// pink
r = 250;
g = 0;
b = 255;
} else if (dataArray[i] > 200) {
// yellow
r = 250;
g = 255;
b = 0;
} else if (dataArray[i] > 190) {
// yellow/green
r = 204;
g = 255;
b = 0;
} else if (dataArray[i] > 180) {
// blue/green
r = 0;
g = 219;
b = 131;
} else {
// light blue
r = 0;
g = 199;
b = 255;
}
ctx1.fillStyle = `rgb(${r},${g},${b})`;
ctx1.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
ctx2.fillStyle = `rgb(${r},${g},${b})`;
ctx2.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
x += barWidth + 10;
}
}
renderFrame();
I want to rotate ctx around Y-axis not canvas.
Is there possible way? or do i have to use WebGL?
If you ask why i need this,
I want canvas and context to fit in the left screen of the image.
what could be the best way to make canvas to be fit in?
Oh, i'm using requestAnimationFrame to keep drawing something on context.
That's why I need to rotate the context, not only the canvas.
This question already has an answer here:
Unable to update HTML5 canvas pixel color using putImageData in JavaScript
(1 answer)
Closed last year.
I am trying to draw a gray gradient (or a few of them actually) to the canvas. To do this I am using the getImageData() and putImageData() methods of canvas. data is an Uint8ClampedArray with size 1000 * 1000 * 4. In the for loops the rgb color elements of data are correctly set to be x%255, as shown by printing the data array to console.
However the result of code differs from what is expected. A completely white cnvas is shown while 4 gray gradients are expected.
Minimal working example:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<canvas id = "canvas" width = "1000" height = "1000"></canvas>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
draw();
function draw(){
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for(let y = 0; y < canvas.height; y++){
for(let x = 0; x < canvas.width; x++){
for(let color = 0; color < 3; color++){
let idx = (y * canvas.width + x) * 4;
data[idx + color] = x % 255;
}
}
}
ctx.putImageData(imageData, 0, 0);
}
</script>
</body>
</html>
You're not setting the alpha channel.
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
let idx = (y * canvas.width + x) * 4;
for (let color = 0; color < 3; color++) {
data[idx + color] = x % 255;
}
data[idx + 3] = 255; // alpha
}
}
Am trying to calculate width and height of object i loaded into canvas. When object is not rotated i get correct left right top bottom values, but when i load rotated object in canvas then i not get correct values , so i wonder what will be the logic or math formula to do achieve it.
how am doing.
initially load image into canvas
get image data from canvas
loop through image data to get only colored pixels by using alpha check
from colored pixel array find min max xy values
var temp_ray = []; // pixel array
for (var y = 0; y < imgData.height; ++y) {
for (var x = 0; x < imgData.width; ++x) {
var index = (y * imgData.width + x) * 4;
if(imgData.data[index+3]){
var xc = (index / 4) % imgData.width;
var yc = Math.floor((index / 4) / imgData.width);
temp_ray.push([xc,yc]);
}
}
}
if(temp_ray.length > 0){
var Xind = MaxMin2darray(temp_ray,0);
var Yind = MaxMin2darray(temp_ray,1);
var W = parseFloat(Xind['max']) - parseFloat(Xind['min']);
var H = parseFloat(Yind['max']) - parseFloat(Yind['min']);
var center_x = Xind['min'] + (W/2);
var center_y = Yind['min'] + (H/2);
// find corners of object
// find *min x , min y
let top_left = temp_ray[Xind['imin']]; // min X priority , min Y // top left
// find max x , *min y
let top_right = temp_ray[Yind['imin']]; // max X, min Y priority , // top right
// find *max x , min y
let bot_right = temp_ray[Xind['imax']]; // max X priority , min Y // bottom right
// find max x , *max y
let bot_left = temp_ray[Yind['imax']]; // max X , max Y priority // bottom left
var dim = {'W':W,'H':H,'CenterX':center_x,'CenterY':center_y,'top_left':top_left,'top_right':top_right,'bot_right':bot_right,'bot_left':bot_left,'Xend':Xind['max'],'Yend':Yind['max'],'Xstart':Xind['min'],'Ystart':Yind['min'],'Xend':Xind['max'],'Yend':Yind['max']};
console.log(dim);
}
and then using min max xy value find corners of object which works with none rotated objects but not work with rotated/tilted objects.
so any idea how to solve this problem
openpnp project is achieving this through opencv, but i think in js we do not have opencv library nor am that pro of java :(.
https://github.com/openpnp/openpnp/blob/develop/src/main/java/org/openpnp/vision/pipeline/stages/DrawRotatedRects.java
jsfiddle: http://jsfiddle.net/4L13vtaj/
In some simple cases (like rectangular objects), you could try to rotate the image until you minimize the number of uncolored pixels.
So you start with your image, and for each of the possible 360°, you compute the ratio. This is not perfect, but "doable" simply in pure js.
Here's a pseudoCode that might help you:
for degree in [0,365]{
rotateOriginalImageBy(degree);
cost[degree] = NemptyPixels/NfilledPixels;
}
predictedDegree = Math.min(cost);
rotateOriginalImageBy(predictedDegree);
compute 2 dimensions;
width = largerDimension;
height = shorterDimension;
Begining of an implementation (I edited your jsfiddle):
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var rotatioDegree = 45;
var imageObject = new Image();
imageObject.onload = function() {
var canvasWidth = imageObject.width;
var canvasHeight = canvasWidth; // not useful since width==height
document.getElementById('canvas').width = canvasWidth;
document.getElementById('canvas').height = canvasWidth;
ctx.clearRect(0, 0, canvasWidth, canvasWidth);
// Move registration point to the center of the canvas
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
ctx.drawImage(imageObject,0,0);
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(-rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
var imgData = ctx.getImageData(0, 0, canvasWidth, canvasWidth);
http://jsfiddle.net/4L13vtaj/17/
If this doesn't work, you could implement some image detection techniques (Mathematical morphology for example). But i think this is outside the scope of stackoverflow.
If you work with some approximation, you can have something like that; I hope at least it can provide to you some ideas:
// some pixels in this image are not transparent, so we add a tollerance
// you can try to remove the second condition.
const isNotEmpty = (color) => color && color < 0xffaaaaaa;
function getTop(buff, w, h) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getRight(buff, w, h) {
for (let x = w; x >=0; x--) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getBottom(buff, w, h) {
for (let y = h; y >= 0; y--) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getLeft(buff, w, h) {
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
async function main(imageSource) {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const imageObject = new Image();
imageObject.src = imageSource;
await new Promise(r => imageObject.onload = r);
const w = canvas.width = imageObject.width;
const h = canvas.height = imageObject.height;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(imageObject, 0, 0);
const imgData = ctx.getImageData(0, 0, w, h);
const buff = new Uint32Array(imgData.data.buffer);
const points = [
getTop(buff, w, h),
getRight(buff, w, h),
getBottom(buff, w, h),
getLeft(buff, w, h)
];
ctx.strokeStyle = "#0000ff"
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.lineTo(points[2].x, points[2].y);
ctx.lineTo(points[3].x, points[3].y);
ctx.closePath();
ctx.stroke();
}
main(/* image's url*/);
Here the link for testing: https://codepen.io/zer0/pen/zLxyQV
There are several problem with this approach: as said, with irregular images, it's not precise, in fact you will see the pin are making the image's bounding box a little bit smaller.
But the thing can be worse: try in the link above to use the 2nd image, that is quite irregular, and you will see.
Of course we can compensate, using also a bit more complex algorithm instead this simple one, but the question is: what the expected result for something like the 2nd image? Depends by that you can decide how to proceed.
I am trying to use Javascript to find the darkest region of an image.
So far, this is what I have:
https://jsfiddle.net/brampower/bv78rmz8/
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return ({
h: h,
s: s,
l: l,
})
}
function solve_darkest(url, callback) {
var image = new Image();
image.src = url;
image.onload = function(){
var canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
var imgData = context.getImageData(0, 0, 300, 300);
var pixel = 0;
var darkest_pixel_lightness = 100;
var darkest_pixel_location = 0;
for (var i = 0; i < imgData.data.length; i += 4) {
red = imgData.data[i + 0];
green = imgData.data[i + 1];
blue = imgData.data[i + 2];
alpha = imgData.data[i + 3];
var hsl = rgbToHsl(red, green, blue);
var lightness = hsl.l;
if (lightness < darkest_pixel_lightness) {
darkest_pixel_lightness = lightness;
darkest_pixel_location = pixel;
}
pixel++;
}
var y = Math.floor(darkest_pixel_location/200);
var x = darkest_pixel_location-(y*200);
callback(x,y);
};
}
image_url = 'http://i.imgur.com/j6oJO8s.png';
solve_darkest(image_url, function(x, y) {
alert('x: '+x+' y: '+y);
});
It won't work in JSFiddle because of the tainted canvas, but hopefully that will give you an idea. For the sample image, my JS is currently returning the following coordinates:
x: 140 y: 117
These are not the correct coordinates. The darkest pixel of this image should be around the following coordinates:
x: 95 y: 204
I just can't figure out why the coordinates are so off. Anyone here that would be willing to shed some light on what I'm doing wrong?
Ok, I just tested your jsfiddle.
For the tainted canvas just change crossOrigin property:
var image = new Image();
image.crossOrigin = "Anonymous";
For the incorrect pixel, there are several problems.
Incorrect canvas size. If the image is smaller than the canvas size, the algorithm tests pixels which are not in the image, but are in the canvas. Since you don't drop the pixels which are transparent, you also test the 0, 0, 0 (RGB) pixel which is supposed to be black #000000.
Incorrect 1-dimensional array to 2-dimensional transformation. The formula you are using is incorrect, because you set the width and height to 300, but use 200 in the formula. I suggest creating a variable and using that as a reference.
If you doubt that the pixel is exactly there, create a small picture, like 5x5 px size and check if the algorithm returns what you expect.
I updated the jsfiddle, I think this is correct now. Also, removed the img element in HTML and just appended the canvas to the body: https://jsfiddle.net/Draznel/597u5h0c/1/
Without the JSFiddle working, my best guess would be that the logic in
var y = Math.floor(darkest_pixel_location/200);
var x = darkest_pixel_location-(y*200);
is incorrect for two reasons.
1) The image is 300 pixels in width/height, not 200
2) The imagedata is ordered by x first and y second
To get the correct x and y coordinates, I think the following code would work:
var x = Math.floor(darkest_pixel_location / imageWidth);
var y = darkest_pixel_location % imageWidth;
A good time to realise the nasty consequences of hard-coded variables...
You're creating a canvas of 300x300 and drawing a png of the same dimensions to it. Unfortunately, you then use 200 to determine the x,y pos given the index of the selected pixel. Hint: make a 300x300 image that's white. Set a single pixel at (95,204) to black. Run your unmodified code and you'll get 95,306. Change to same as image size and you'll get (95,204) as your answer.
So, you can hardly complain that the index into a 300x300 image returns the wrong position when manipulated in a way that would be appropriate for the index into a 200x200 image.
Personally, I'd replace:
var y = Math.floor(darkest_pixel_location/200);
var x = darkest_pixel_location-(y*200);
with
var y = Math.floor(darkest_pixel_location/this.width);
var x = darkest_pixel_location-(y*this.width);
That will then convert an index back into correct x,y coordinates.
That said, the image you've provided actually doesn't have it's darkest spot at the indicated location. The pixel at 95,204 has the value of #37614e, or rgb(55,97,78).
Thus, I hope you can now see the purpose of my suggestion of a single dark pixel in an otherwise light image. You can reduce the number of issues you're trying to debug to one at a time. In this case - "can I convert an index back into 2d coordinates?". Once done, "do I have a dark spot actually where I think it is?"
In your case - the answer to both of these questions was no! Not exactly the most advantageous place from which to start debugging...
some comments were made and a better understanding of the problem at hand gained.
Okay, as per the discussion in the comments - the task is to find the upper-left corner of a darkened region in an image - such a search should average results over an (as yet) unknown area such that local minimums or maximums (dark or light pixels) will not adversely affect the identified area of interest.
Ideally, one may run the code iteratively - try with a block-size of 1, then a block-size of 2, etc, etc, increasing the block-size until the results from two runs are the same or within a certain limit.
I.e if I search with a block-size of 9 and get the location 93,211 and then get the same with a block-size of 10 (whereas all previous block-size values returned a different result) then I'd probably feel fairly confident I'd correctly identified the area of interest.
Here's some code to chew on. You'll notice I've left your function and created another, very similar one. I hope you'll find it suitable. :)
"use strict";
function newEl(tag){return document.createElement(tag)}
function newTxt(txt){return document.createTextNode(txt)}
function byId(id){return document.getElementById(id)}
function allByClass(clss,parent){return (parent==undefined?document:parent).getElementsByClassName(clss)}
function allByTag(tag,parent){return (parent==undefined?document:parent).getElementsByTagName(tag)}
function toggleClass(elem,clss){elem.classList.toggle(clss)}
function addClass(elem,clss){elem.classList.add(clss)}
function removeClass(elem,clss){elem.classList.remove(clss)}
function hasClass(elem,clss){elem.classList.contains(clss)}
// useful for HtmlCollection, NodeList, String types
function forEach(array, callback, scope){for (var i=0,n=array.length; i<n; i++)callback.call(scope, array[i], i, array);} // passes back stuff we need
// callback gets data via the .target.result field of the param passed to it.
function loadFileObject(fileObj, loadedCallback){var a = new FileReader();a.onload = loadedCallback;a.readAsDataURL( fileObj );}
function ajaxGet(url, onLoad, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onLoad(this);}
ajax.onerror = function(){console.log("ajax request failed to: "+url);onError(this);}
ajax.open("GET",url,true);
ajax.send();
}
function ajaxPost(url, phpPostVarName, data, onSucess, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){ onSucess(this);}
ajax.onerror = function() {console.log("ajax request failed to: "+url);onError(this);}
ajax.open("POST", url, true);
ajax.setRequestHeader("Content-type","application/x-www-form-urlencoded");
ajax.send(phpPostVarName+"=" + encodeURI(data) );
}
function ajaxPostForm(url, formElem, onSuccess, onError)
{
var formData = new FormData(formElem);
ajaxPostFormData(url, formData, onSuccess, onError)
}
function ajaxPostFormData(url, formData, onSuccess, onError)
{
var ajax = new XMLHttpRequest();
ajax.onload = function(){onSuccess(this);}
ajax.onerror = function(){onError(this);}
ajax.open("POST",url,true);
ajax.send(formData);
}
function getTheStyle(tgtElement)
{
var result = {}, properties = window.getComputedStyle(tgtElement, null);
forEach(properties, function(prop){result[prop] = properties.getPropertyValue(prop);});
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt)
{
// var image_url = 'http://i.imgur.com/j6oJO8s.png';
// var image_url = 'onePixel.png';
var image_url = 'j6oJO8s.png';
byId('theImage').src = image_url;
solve_darkest(image_url, function(x,y){alert('x: '+x+' y: '+y);} );
solve_darkest_2(image_url, function(x,y){alert('x: '+x+' y: '+y);} );
}
function rgbToHsl(r, g, b)
{
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return ({
h: h,
s: s,
l: l,
})
}
function solve_darkest(url, callback)
{
var image = new Image();
image.src = url;
image.onload = function()
{
var canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
var imgData = context.getImageData(0, 0, 300, 300);
var pixel = 0;
var darkest_pixel_lightness = 100;
var darkest_pixel_location = 0;
for (var i = 0; i < imgData.data.length; i += 4)
{
var red = imgData.data[i + 0];
var green = imgData.data[i + 1];
var blue = imgData.data[i + 2];
var alpha = imgData.data[i + 3];
var hsl = rgbToHsl(red, green, blue);
var lightness = hsl.l;
if (lightness < darkest_pixel_lightness)
{
darkest_pixel_lightness = lightness;
darkest_pixel_location = pixel;
console.log("Darkest found at index: " + pixel);
}
pixel++;
}
// var y = Math.floor(darkest_pixel_location/200);
// var x = darkest_pixel_location-(y*200);
var y = Math.floor(darkest_pixel_location/this.width);
var x = darkest_pixel_location-(y*this.width);
callback(x,y);
};
}
function solve_darkest_2(url, callback)
{
var image = new Image();
image.src = url;
image.onload = function()
{
var canvas = document.createElement('canvas');
canvas.width = 300;
canvas.height = 300;
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
var imgData = context.getImageData(0,0, canvas.width, canvas.height);
var darkest_pixel_luminance = 100;
var darkest_pixel_xPos = 0;
var darkest_pixel_yPos = 0;
for (var y=0; y<canvas.height; y++)
{
for (var x=0; x<canvas.width; x++)
{
var luminance = averagePixels(imgData, x, y, 10);
if (luminance < darkest_pixel_luminance)
{
darkest_pixel_luminance = luminance;
darkest_pixel_xPos = x;
darkest_pixel_yPos = y;
}
}
}
callback(darkest_pixel_xPos,darkest_pixel_yPos);
};
}
function averagePixels(imgData, xPos, yPos, averagingBlockSize)
{
// var ctx = canvas.getContext("2d");
// var imgData = ctx.getImageData( 0, 0, canvas.width, canvas.height );
// imgData
// we average pixels found in a square region, we need to know how many pixels
// are in the region to divide the accumalated totals by the number of samples (pixels) in the
// averaging square
var numPixelsMax = averagingBlockSize * averagingBlockSize;
var numPixelsActual = 0;
var red, green, blue;
red = green = blue = 0;
var rowStride = imgData.width * 4; // add this to an index into the canvas's data to get the pixel
// immediatelly below it.
var x, y;
var initialIndex = ((yPos * imgData.width) + xPos) * 4;
var index = initialIndex;
var pixel = 0;
var darkest_pixel_lightness = 100;
var darkest_pixel_location = 0;
for (y=0; y<averagingBlockSize; y++)
{
index = initialIndex + y * rowStride;
for (x=0; x<averagingBlockSize; x++)
{
if ((x+xPos < imgData.width) && (y+yPos < imgData.height))
{
red += imgData.data[index+0];
green += imgData.data[index+1];
blue += imgData.data[index+2];
numPixelsActual++;
}
index += 4;
}
}
red /= numPixelsActual;
green /= numPixelsActual;
blue /= numPixelsActual;
var hsl = rgbToHsl(red, green, blue);
var luminance = hsl.l;
return luminance;
}
img
{
border: solid 1px red;
}
<h1>300px</h1>
<img id='theImage'/>
Let's say this is my canvas, with an evil-looking face drawn on it. I want to use toDataURL() to export my evil face as a PNG; however, the whole canvas is rasterised, including the 'whitespace' between the evil face and canvas edges.
+---------------+
| |
| |
| (.Y. ) |
| /_ |
| \____/ |
| |
| |
+---------------+
What is the best way to crop/trim/shrinkwrap my canvas to its contents, so my PNG is no larger than the face's 'bounding-box', like below? The best way seems to be scaling the canvas, but supposing the contents are dynamic...? I'm sure there should be a simple solution to this, but it's escaping me, with much Googling.
+------+
|(.Y. )|
| /_ |
|\____/|
+------+
Thanks!
Edited (see comments)
function cropImageFromCanvas(ctx) {
var canvas = ctx.canvas,
w = canvas.width, h = canvas.height,
pix = {x:[], y:[]},
imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index+3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a,b){return a-b});
pix.y.sort(function(a,b){return a-b});
var n = pix.x.length-1;
w = 1 + pix.x[n] - pix.x[0];
h = 1 + pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
var image = canvas.toDataURL();
}
If I understood well you want to "trim" away all the surronding your image / drawing, and adjust the canvas to that size (like if you do a "trim" command in Photoshop).
Here is how I'll do it.
Run thru all the canvas pixels checking if their alpha component is > 0 (that means that something is drawn in that pixel). Alternativelly you could check for the r,g,b values, if your canvas background is fullfilled with a solid color, for instance.
Get te coordinates of the top most left pixel non-empty, and same for the bottom most right one. So you'll get the coordinates of an imaginay "rectangle" containing the canvas area that is not empty.
Store that region of pixeldata.
Resize your canvas to its new dimensions (the ones of the region we got at step 2.)
Paste the saved region back to the canvas.
Et, voilá :)
Accesing pixeldata is quite slow depending on the size of your canvas (if its huge it can take a while). There are some optimizations around to work with raw canvas pixeldata (I think there is an article about this topic at MDN), I suggest you to google about it.
I prepared a small sketch in jsFiddle that you can use as starting point for your code.
Working sample at jsFiddle
Hope I've helped you.
c:.
Here's my take. I felt like all the other solutions were overly complicated. Though, after creating it, I now see it's the same solution as one other's, expect they just shared a fiddle and not a function.
function trimCanvas(canvas){
const context = canvas.getContext('2d');
const topLeft = {
x: canvas.width,
y: canvas.height,
update(x,y){
this.x = Math.min(this.x,x);
this.y = Math.min(this.y,y);
}
};
const bottomRight = {
x: 0,
y: 0,
update(x,y){
this.x = Math.max(this.x,x);
this.y = Math.max(this.y,y);
}
};
const imageData = context.getImageData(0,0,canvas.width,canvas.height);
for(let x = 0; x < canvas.width; x++){
for(let y = 0; y < canvas.height; y++){
const alpha = imageData.data[((y * (canvas.width * 4)) + (x * 4)) + 3];
if(alpha !== 0){
topLeft.update(x,y);
bottomRight.update(x,y);
}
}
}
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
const croppedCanvas = context.getImageData(topLeft.x,topLeft.y,width,height);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas,0,0);
return canvas;
}
Here's code in ES syntax, short, fast and concise:
/**
* Trim a canvas.
*
* #author Arjan Haverkamp (arjan at avoid dot org)
* #param {canvas} canvas A canvas element to trim. This element will be trimmed (reference)
* #param {int} threshold Alpha threshold. Allows for trimming semi-opaque pixels too. Range: 0 - 255
* #returns {Object} Width and height of trimmed canvcas and left-top coordinate of trimmed area. Example: {width:400, height:300, x:65, y:104}
*/
const trimCanvas = (canvas, threshold = 0) => {
const ctx = canvas.getContext('2d'),
w = canvas.width, h = canvas.height,
imageData = ctx.getImageData(0, 0, w, h),
tlCorner = { x:w+1, y:h+1 },
brCorner = { x:-1, y:-1 };
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (imageData.data[((y * w + x) * 4) + 3] > threshold) {
tlCorner.x = Math.min(x, tlCorner.x);
tlCorner.y = Math.min(y, tlCorner.y);
brCorner.x = Math.max(x, brCorner.x);
brCorner.y = Math.max(y, brCorner.y);
}
}
}
const cut = ctx.getImageData(tlCorner.x, tlCorner.y, brCorner.x - tlCorner.x, brCorner.y - tlCorner.y);
canvas.width = brCorner.x - tlCorner.x;
canvas.height = brCorner.y - tlCorner.y;
ctx.putImageData(cut, 0, 0);
return {width:canvas.width, height:canvas.height, x:tlCorner.x, y:tlCorner.y};
}
The top voted answer here, as well as the implementations i found online trim one extra pixel which was very apparent when trying to trim text out of canvas. I wrote my own that worked better for me:
var img = new Image;
img.onload = () => {
var canvas = document.getElementById('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
document.getElementById('button').addEventListener('click', ()=>{
autoCropCanvas(canvas, ctx);
document.getElementById('button').remove();
});
};
img.src = 'data:image/png;base64,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';
function autoCropCanvas(canvas, ctx) {
var bounds = {
left: 0,
right: canvas.width,
top: 0,
bottom: canvas.height
};
var rows = [];
var cols = [];
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvas.width; x++) {
cols[x] = cols[x] || false;
for (var y = 0; y < canvas.height; y++) {
rows[y] = rows[y] || false;
const p = y * (canvas.width * 4) + x * 4;
const [r, g, b, a] = [imageData.data[p], imageData.data[p + 1], imageData.data[p + 2], imageData.data[p + 3]];
var isEmptyPixel = Math.max(r, g, b, a) === 0;
if (!isEmptyPixel) {
cols[x] = true;
rows[y] = true;
}
}
}
for (var i = 0; i < rows.length; i++) {
if (rows[i]) {
bounds.top = i ? i - 1 : i;
break;
}
}
for (var i = rows.length; i--; ) {
if (rows[i]) {
bounds.bottom = i < canvas.height ? i + 1 : i;
break;
}
}
for (var i = 0; i < cols.length; i++) {
if (cols[i]) {
bounds.left = i ? i - 1 : i;
break;
}
}
for (var i = cols.length; i--; ) {
if (cols[i]) {
bounds.right = i < canvas.width ? i + 1 : i;
break;
}
}
var newWidth = bounds.right - bounds.left;
var newHeight = bounds.bottom - bounds.top;
var cut = ctx.getImageData(bounds.left, bounds.top, newWidth, newHeight);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.putImageData(cut, 0, 0);
}
<canvas id=canvas style='border: 1px solid pink'></canvas>
<button id=button>crop canvas</button>