Get exact size of text on a canvas in JavaScript - javascript

I hope there is someone out there to help me with this:
I need to get the exact size of a text. Just measuring a span or so not precise enough for my purposes.
Right now, I am using a canvas to find the non-transparent pixels in the canvas.
This is my code:
// a function to draw the text on the canvas
let text = "Hello World";
let canvas = document.getElementById('happy-canvas');
let width = 1000
let height = 100
canvas.width = width
canvas.height = height
let ctx = canvas.getContext('2d');
ctx.save();
ctx.font = "30px cursive";
ctx.clearRect(0, 0, width, height);
ctx.fillText(text, 0, 60);
// get the image data
let data = ctx.getImageData(0, 0, width, height).data,
first = false,
last = false,
r = height,
c = 0
// get the width of the text and convert it to an integer
const canvWidth = parseInt(ctx.measureText(text).width)
//Find the last line with a non-transparent pixel
while (!last && r) {
r--
for (c = 0; c < width; c++) {
if (data[r * width * 4 + c * 4 + 3]) {
last = r
break
}
}
}
let canvasHeight = 0
// Find the first line with a non-transparent pixel
while (r) {
r--
for (c = 0; c < width; c++) {
if (data[r * width * 4 + c * 4 + 3]) {
first = r
break
}
}
canvasHeight = last - first
}
//draw a rectangle around the text
ctx.strokeRect(0, first, canvWidth, canvasHeight)
<div> The last "d" is not completely inside of the the box
<canvas id="happy-canvas" width="150" height="150"> I wonder what is here</canvas>
</div>
This works to get the exact height of the text, but not the width.
So I use "measureText" right now, but that function gets different sizes depending on the browser and on the font I use.
If I use a reagular font, it works quite well. But if I use a more playful font, it does not work at all.
Here is an example:
https://i.imgur.com/ySOIbDR.png
The black box is the measured size. And as you can see "measureText" does not get the correct width.
Right now I am out of any idea, what else I could do.

Ok, so I just got it working.
What am I doing?
Well, in my case I know, that the text will always start at a x-value of 0.
The length of the text is therefore the non-transparent pixel with the highest x-value in the array given by getImageData().
So I am looping through the getImageData()-array. If I find a pixel that has a higher alpha-value than 0, I will save its x and y value into highestPixel. The next time I find a pixel, I will check if its x-value is higher as the one that is currently in highestPixel. If so, I will overwrite highestPixel with the new values. At the end, I return highestPixel and its x-value will be the exact length of the text.
Here is the code:
// a function to draw the text on the canvas
let text = "Hello World";
let canvas = document.getElementById('happy-canvas');
let width = 1000
let height = 100
canvas.width = width
canvas.height = height
let ctx = canvas.getContext('2d');
ctx.save();
ctx.font = "30px cursive";
ctx.clearRect(0, 0, width, height);
ctx.fillText(text, 0, 60);
// get the image data
let data = ctx.getImageData(0, 0, width, height).data,
first = false,
last = false,
r = height,
c = 0
// get the width of the text and convert it to an integer
let getPixelwithHighestX = () => {
let xOfPixel = 0
let yOfPixel = 0
let highestPixel = {
x: 0,
y: 0
}
for (let i = 3; i < data.length; i += 4) {
if (data[i] !== 0) {
yOfPixel = Math.floor(i / 4 / width)
xOfPixel = Math.floor(i / 4) - yOfPixel * width
if (xOfPixel > highestPixel.x) {
highestPixel.x = xOfPixel
highestPixel.y = yOfPixel
}
}
}
return highestPixel
}
let hightestPixel = getPixelwithHighestX()
//Find the last line with a non-transparent pixel
while (!last && r) {
r--
for (c = 0; c < width; c++) {
if (data[r * width * 4 + c * 4 + 3]) {
last = r
break
}
}
}
let canvasHeight = 0
// Find the first line with a non-transparent pixel
while (r) {
r--
for (c = 0; c < width; c++) {
if (data[r * width * 4 + c * 4 + 3]) {
first = r
break
}
}
canvasHeight = last - first
}
//draw a rectangle around the text
ctx.strokeRect(0, first, hightestPixel.x, canvasHeight)
<div> The text is now completely inside the box
<canvas id="happy-canvas" width="150" height="150"> I wonder what is here</canvas>
</div>

Related

A Javascript Canvas Loop to Check If Images Have a White Background

I want to loop over about 50 images in HTML, extract the src from each, check if the dominant color of the image background is white, then based on the results add some css styling (e.g padding).
So far I have this code however for some reason it's not working. The code works separately but when placed in a for loop it doesn't work. Usually, this loop either doesn't work at all or works only till some point then just feeds out a default result of "#FFFFFF" because the canvas itself is filled white.
I'm not sure why it's not working. I've been trying to fix it but to no avail.
DEMO HERE (please look through) : https://jsbin.com/tucegomemi/1/edit?html,js,console,output
JAVASCRIPT HERE :
var i;
var GlobalVariable;
for (i=0; i < document.querySelectorAll('img').length ; i++) {
let canvas = document.getElementById("canvas"),
canvasWidth = canvas.width,
canvasHeight = canvas.height,
c = canvas.getContext("2d"),
img = new Image();
img.crossOrigin="anonymous";
img.src = document.querySelectorAll('img')[i].src
// Prepare the canvas
var ptrn = c.createPattern(img, 'repeat');
c.fillStyle = "white";
c.fillRect(0,0,canvasWidth,canvasHeight);
c.fillStyle = ptrn;
c.fillRect(0,0,canvasWidth,canvasHeight);
// Get img data
var imgData = c.getImageData(0, 0, canvasWidth, canvasHeight),
data = imgData.data,
colours = {};
// Build an object with colour data.
for (var y = 0; y < canvasHeight; ++y) {
for (var x = 0; x < canvasWidth; ++x) {
var index = (y * canvasWidth + x) * 4,
r = data[index],
g = data[++index],
b = data[++index],
rgb = rgbToHex(r,g,b);
if(colours[rgb]){
colours[rgb]++;
}else{
colours[rgb] = 1;
}
}
}
// Determine what colour occurs most.
var most = {
colour:'',
amount:0
};
for(var colour in colours){
if(colours[colour] > most.amount){
most.amount = colours[colour];
most.colour = colour;
}
}
GlobalVariable = most.colour;
console.log(i);
console.log(GlobalVariable);
if (GlobalVariable !== "#ffffff") {document.querySelectorAll('img')[i].style.padding = "50px" ;}
}
function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
Wait for images to load
As the images are on the page you can wait for the page load event to fire. This will fire only when all the images have loaded (or fail to load). Do read the link as there are some caveats to using the load event.
Also as the images are on the page there is no need to create a copy of the image using new Image You can use the image directly from the page.
Also I assume that all images will load. If image do not load there will be problems
Looking at your code it is horrifically inefficient, thus the example is a complete rewrite with an attempt to run faster and chew less power.
Note: that the example uses a temp canvas that is in memory only. It does not need a canvas on the page.
Note: that it stop counting if a pixel has a count greater than half the number of pixels in the image.
addEventListener("load",() => { // wait for page (and images to load)
const toHex = val => (val & 0xFF).toString(16).padStart(2,"0"); // mask the to hex and pad with 0 if needed
const pixel2CSScolor = px => `#${toHex(px >> 16)}${toHex(px >> 8)}${toHex(px)}`;
const images = document.querySelectorAll('img');
const canvas = document.createElement("canvas"); // Only need one canvas
const ctx = canvas.getContext("2d"); // and only one context
for (const image of images) { // do each image in turn
const w = canvas.width = image.naturalWidth; // size to fit image
const h = canvas.height = image.naturalHeight;
ctx.fillStyle = "#FFF";
ctx.fillRect(0, 0, w, h);
ctx.drawImage(image, 0, 0);
const imgData = ctx.getImageData(0, 0, w, h);
const pixels = new Uint32Array(imgData.data.buffer); // get a pixel view of data (RGBA as one number)
const counts = {}; // a map of color counts
var idx = pixels.length, maxPx, maxCount = 0; // track the most frequent pixel count and type
while (idx-- > 0) {
const pixel = pixels[idx]; // get pixel
const count = counts[pixel] = counts[pixel] ? counts[pixel] + 1 : 1;
if (count > maxCount) {
maxCount = count;
maxPx = pixel;
if (count > pixels.length / 2) { break }
}
}
image._FOUND_DOMINATE_COLOR = pixel2CSScolor(maxPx);
}
});
Each image has a new property attached called _FOUND_DOMINATE_COLOR which hold a string with the colour as a CSS hex color
An even better way
As I am unsure of the image format and the content of the image the above example is the cover all solution.
If the images have large areas of similar color, or the image has a lot of noise you can use the GPU rendering to do most of the counting for you. This is done by drawing the image at progressively smaller scales. The drawImage function will average the pixel values as it does.
This means that when your code looks at the pixel data the is a lot less, Half the image size and there is 4 times less memory and CPU load, quarter the size and 16 times less work.
The next example reduces the image to 1/4 its natural size and then uses the averaged pixel values to find the color. Note that for the best results the images should be at least larger than 16 pixels in width and height
addEventListener("load",() => {
const toHex = val => (val & 0xFF).toString(16).padStart(2,"0");
const pixel2CSScolor = px => `#${toHex(px >> 16)}${toHex(px >> 8)}${toHex(px)}`;
const reduceImage = image => {
const w = canvas.width = image.naturalWidth;
const h = canvas.height = image.naturalHeight;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "#FFF";
ctx.fillRect(0, 0, w, h);
ctx.drawImage(image, 0, 0);
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, w / 2, h / 2);
ctx.drawImage(canvas, 0, 0, w / 2, h / 2, 0, 0, w / 4, h / 4);
return new Uint32Array(ctx.getImageData(0, 0, w / 4 | 0, h / 4 | 0).data.buffer);
}
const images = document.querySelectorAll('img');
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
for (const image of images) {
const pixels = reduceImage(image), counts = {};
var idx = pixels.length, maxPx, maxCount = 0;
while (idx-- > 0) {
const pixel = pixels[idx]; // get pixel
const count = counts[pixel] = counts[pixel] ? counts[pixel] + 1 : 1;
if (count > maxCount) {
maxCount = count;
maxPx = pixel;
if (count > pixels.length / 2) { break }
}
}
image._FOUND_DOMINATE_COLOR = pixel2CSScolor(maxPx);
}
});
Update
As there were some questions in the comments the next snippet is a check to make sure all is working.
I could not find any problems with the code apart from the correction I detailed in the comments.
I did change some names and increased the image reduction steps a lot more for reasons outlined under the next heading
Color frequency does not equal dominate color
The example below shows two images, when loaded the padding is set to the color found. You will note that the image on the right does not seem to get the color right.
This is because there are many browns yet no one brown is the most frequent.
In the my answer Finding dominant hue. I addressed the problem and found a solution that is more in tune with human perception.
Working example
. Warning for low end devices. one of the images is ~9Mpx .
addEventListener("load",() => { geMostFrequentColor() },{once: true});
const downScaleSteps = 4;
function geMostFrequentColor() {
const toHex = val => (val & 0xFF).toString(16).padStart(2,"0");
const pixel2CSScolor = px => `#${toHex(px >> 16)}${toHex(px >> 8)}${toHex(px)}`;
const reduceImage = image => {
var w = canvas.width = image.naturalWidth, h = canvas.height = image.naturalHeight, step = 0;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "#FFF";
ctx.fillRect(0, 0, w, h);
ctx.drawImage(image, 0, 0);
ctx.globalCompositeOperation = "copy";
while (step++ < downScaleSteps) {
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, w /= 2, h /= 2);
}
return new Uint32Array(ctx.getImageData(0, 0, w | 0, h | 0).data.buffer);
}
const images = document.querySelectorAll('img');
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
var imgCount = 0;
for (const image of images) {
info.textContent = "Processing image: " + imgCount++;
const pixels = reduceImage(image), counts = {};
let idx = pixels.length, maxPx, maxCount = 0;
while (idx-- > 0) {
const pixel = pixels[idx]; // get pixel
const count = counts[pixel] = counts[pixel] ? counts[pixel] + 1 : 1;
if (count > maxCount) {
maxCount = count;
maxPx = pixel;
if (count > pixels.length / 2) { break }
}
}
image._MOST_FREQUENT_COLOR = pixel2CSScolor(maxPx);
image.style.background = image._MOST_FREQUENT_COLOR;
}
info.textContent = "All Done!";
}
img {
height: 160px;
padding: 20px;
}
<div id="info">Loading...</div>
<img src="https://upload.wikimedia.org/wikipedia/commons/d/dd/Olympus-BX61-fluorescence_microscope.jpg" crossorigin="anonymous">
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a5/Compound_Microscope_(cropped).JPG" alt="Compound Microscope (cropped).JPG" crossorigin="anonymous"><br>
Images from wiki no attribution required.
You're currently drawing an empty image. The image needs some time to load so you'll have to wait for that to happen.
Use the onload callback to draw the image to the canvas as soon as it has finished loading. Every other process should continue after this event.
img = new Image();
img.crossOrigin = "anonymous";
img.src = document.querySelectorAll('img')[i].src;
img.onload = function() {
c.clearRect(0, 0, canvasWidth, canvasHeight);
c.drawImage(img, 0, 0, canvasWidth, canvasHeight);
// Continue here
}

Detect radial Pixel

I Wan't to check the collision from radial Elements.
The Problem is, currently i check only the pixels as an rectangle because the other images are native with HTML-Elements.
I'm only using the canvas to draw the boundary background to check the alpha-transparency.
this.checkCollision = function checkCollision() {
var width = 34;
var height = 32;
var image = _context.getImageData(_position.x - (height / 2), _position.y - (width / 2), width, height);
var pixels = image.data;
var size = image.data.length;
// HERE I WANT TO CHECK A RADIAL AREA
for(var index = 0; index < size; index += 4) {
var RED = image.data[index];
var GREEN = image.data[index + 1];
var BLUE = image.data[index + 2];
var ALPHA = image.data[index + 3];
if(_debug) {
document.querySelector('#debug').innerHTML = JSON.stringify({
POSITION: _position,
INDEX: index,
COLOR: {
R: RED,
G: GREEN,
B: BLUE,
A: ALPHA
}
}, 0, 1);
}
if(ALPHA !== 0) {
return true;
}
}
_context.putImageData(image, 0, 0);
return false;
};
Preview
Here is a working Fiddle:
https://jsfiddle.net/2bLfd6xp/
How i can select a radial pixel range on getImageData to check the collision with the alpha-transparency from the boundary.png?
My idea is to modify the pixel data array from here:
var image = _context.getImageData(_position.x - (height / 2), _position.y - (width / 2), width, height);
and remove the invisible edges. But what is the best practice to calculate from an rectangle based pixel array an radial area to remove these unwanted pixels?
For sample:
var image = _context.getImageData(_position.x - (height / 2), _position.y - (width / 2), width, height);
var radial_area = selectRadialArea(image, radius);
function selectRadialArea(pixelArray, radius) {
/*
Modify `pixelArray` with given `radius`...
All pixels outside the `radius` filled with `null`...
*/
return theNewArray;
}
I've found the answer with logical thinking:
First, we create a temporary drawable context and draw in this new area two elemengts:
an red rectangle
an transparent arc/circle with an destination-composite
The resulted Uint8ClampedArray will be compared with the original Uint8ClampedArray. If the area is RED, we hide the pixels with null-entries:
this.rectangleToRadial = function rectangleToRadial(source, width, height) {
var test = document.createElement('canvas');
var context = test.getContext('2d');
// Create an transparent circle and a red removeable area
context.beginPath();
context.fillStyle = 'rgba(255, 0, 0, 1)';
context.fillRect(0, 0, width, height);
context.globalCompositeOperation = 'destination-out';
context.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI);
context.fillStyle = 'rgba(0, 0, 0, 1)';
context.fill();
// get the data
var destination = context.getImageData(0, 0, width, height);
var size = destination.data.length;
// check the pixels
for(var index = 0; index < size; index += 4) {
var RED = destination.data[index];
var GREEN = destination.data[index + 1];
var BLUE = destination.data[index + 2];
var ALPHA = destination.data[index + 3];
/*
if the >>red removeable area<< is given, null the pixel from the source
*/
if(RED == 255 && GREEN == 0 && BLUE == 0) {
// Remove this from source
source.data[index] = null;
source.data[index + 1] = null;
source.data[index + 2] = null;
source.data[index + 3] = null;
}
}
// Return the source `Uint8ClampedArray`
return source;
};
It was easy, when we try to think :)
var image = _context.getImageData(_position.x - (height / 2), _position.y - (width / 2), width, height);
var pixels = this.rectangleToRadial(image, width, height);

HTML Canvas get total fill in percent

I doing a canvas to restore the original image when cursor moving around, in different percentage to show in different message to tell user. How to calculate the total percentage that already filled?
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
context.beginPath();
context.fillStyle = 'black';
context.fillRect(0, 0, 400, 300);
canvas.onmousedown = function() {
canvas.onmousemove = function() {
var x = event.clientX;
var y = event.clientY;
context.globalCompositeOperation = "destination-out";
context.beginPath();
context.arc(x-0, y, 30, 0, Math.PI*2);
context.fill();
}
}
canvas.onmouseup = function() {
canvas.onmousemove = function() {
//
}
}
<img src="http://blog.honeyfeed.fm/wp-content/uploads/2015/01/onepiece-wallpaper-20160724205402-560x380.jpg" style="width: 400px; height: 300px; position: absolute; z-index: -1;" />
<canvas id="canvas" width="400" height="300"></canvas>
If you want to "brute force" this calculation, you could use getImageData and check the total number of pixels that is transparent.
The main code:
// This returns an array with 4 bytes (0-255) per pixel
// data[0] -> R value of first pixel
// data[1], [2], and [3] -> G, B, and A values
// etc.
const data = context
.getImageData(0, 0, canvas.width, canvas.height)
.data;
// The total number of pixels is the length of the
// data array divided by 4, or width * height
const nrOfPixels = data.length / 4; // rgba pixels
let transparent = 0;
// Your code removes the alpha, so we check each
// 4th item in the array (notice the += 4)
// If it's transparent (A === 0), we count it
for (let i = 3; i < data.length; i += 4) {
transparent += data[i] ? 0 : 1;
}
// The percentage is the number of transparent pixels
// divided by the total number of pixels
const percentage = transparent / nrOfPixels * 100;
This is by no means an optimized way of doing this. That's why, for now, I included it in the mouseup event listener and put a console.time around it.
EDIT: because I felt guilty that I answered a duplicate question with almost the exact same solution as was apparently linked in the comments, I optimized for performance. Now I feel this answer actually adds an additional solution.
The optimization:
We divide our canvas in to a grid of squares size s
One array holds the transparent pixel count per box
Another array holds the top left coordinate for boxes that need recalculation
On every mouse move, we calculate the four corners of a box surrounding our cleared circle
For each corner's x,y location, we check in which of the grid boxes it lies
We mark this grid box as "dirty", which means it has to be checked for changes.
On every mouse move, we use requestAnimationFrame to request a new update calculation
In the update, we do no longer retrieve all image data. Instead, we only request the image data for our dirty grid boxes.
We calculate the transparency for every grid box, add them up and divide by the number of total pixels.
The size of the grid and the size of the brush determine the performance gain of this approach. With the settings in the example below, I was able to get a performance gain of around 400% (4.x ms per calculation to <1ms)
Note that the grid size must be larger than your brush size.
const GRID_SIZE = 50;
const DRAW_SIZE = 30;
var ExposeImage = function(canvas, display) {
const width = canvas.width;
const height = canvas.height;
const cols = width / GRID_SIZE;
const rows = height / GRID_SIZE;
this.gridBlocks = Array(rows * cols);
this.dirtyBlocks = Array(rows * cols);
const gridBlockIndex = (c, r) => r * cols + c;
const rcFromBlockIndex = i => [
Math.floor(i / cols),
i % cols
];
this.context = canvas.getContext("2d");
this.display = display;
this.init();
var logDirtyGridBoxes = function(e) {
var x = e.clientX;
var y = e.clientY;
var r = DRAW_SIZE;
var top = Math.max(y - r, 0);
var bottom = Math.min(y + r, height - 1);
var left = Math.max(x - r, 0);
var right = Math.min(x + r, width - 1);
var corners = [
[top, left],
[top, right],
[bottom, right],
[bottom, left]
];
corners.forEach(c => {
const row = Math.floor(c[0] / GRID_SIZE);
const col = Math.floor(c[1] / GRID_SIZE);
const i = gridBlockIndex(col, row);
this.dirtyBlocks[i] =
/* top left of the grid block */
[col * GRID_SIZE, row * GRID_SIZE];
});
}.bind(this);
var update = function() {
console.time("update");
// Store the transparent pixel count for all our dirty
// grid boxes
this.dirtyBlocks.forEach((coords, i) => {
const data = this.context.getImageData(
coords[0], coords[1], GRID_SIZE, GRID_SIZE).data;
this.gridBlocks[i] = transparentPixelCount(data)
})
// Clear dirty array
this.dirtyBlocks = Array(rows * cols);
// Calculate total average
const total = this.gridBlocks.reduce((sum, b) => sum + b, 0);
const avg = Math.round(
total / (width * height) * 100);
console.timeEnd("update");
display.innerText = avg + "%";
}.bind(this);
// Event listeners
var onMove = function(e) {
this.clear(e.clientX, e.clientY, DRAW_SIZE);
logDirtyGridBoxes(e);
requestAnimationFrame(update);
}.bind(this);
canvas.addEventListener("mousedown", function(e) {
canvas.addEventListener("mousemove", onMove);
onMove(e);
}.bind(this));
canvas.addEventListener("mouseup", function() {
canvas.removeEventListener("mousemove", onMove);
}.bind(this));
};
ExposeImage.prototype.init = function(context) {
this.context.beginPath();
this.context.fillStyle = 'black';
this.context.fillRect(0, 0, 400, 300);
this.context.globalCompositeOperation = "destination-out";
};
ExposeImage.prototype.clear = function(x, y, r) {
this.context.beginPath();
this.context.arc(x - 0, y, r, 0, Math.PI * 2);
this.context.fill();
};
// App:
var canvas = document.getElementById("canvas");
var display = document.querySelector(".js-display");
var ei = new ExposeImage(canvas, display);
function transparentPixelCount(data) {
let transparent = 0;
for (let i = 3; i < data.length; i += 4) {
transparent += data[i] ? 0 : 1;
}
return transparent;
}
<img src="http://blog.honeyfeed.fm/wp-content/uploads/2015/01/onepiece-wallpaper-20160724205402-560x380.jpg" style="width: 400px; height: 300px; position: absolute; z-index: -1;" />
<canvas id="canvas" width="400" height="300"></canvas>
<div class="js-display">0%</div>

How to detect shape on a transparent canvas?

I'm looking for a method of detecting a shape in a transparent PNG.
For example, I will create a transparent canvas of 940x680, then place a fully opaque object somewhere in that canvas.
I want to be able to detect the size (w, h), and top + left location of that object.
Here is an example of the original image:
Here is an example of what I would like to achieve (Bounding box overlay, with top + left margin data):
I've found a resource that does some transparency detection, but I'm not sure how I scale something like this to what I'm looking for.
var imgData,
width = 200,
height = 200;
$('#mask').bind('mousemove', function(ev){
if(!imgData){ initCanvas(); }
var imgPos = $(this).offset(),
mousePos = {x : ev.pageX - imgPos.left, y : ev.pageY - imgPos.top},
pixelPos = 4*(mousePos.x + height*mousePos.y),
alpha = imgData.data[pixelPos+3];
$('#opacity').text('Opacity = ' + ((100*alpha/255) << 0) + '%');
});
function initCanvas(){
var canvas = $('<canvas width="'+width+'" height="'+height+'" />')[0],
ctx = canvas.getContext('2d');
ctx.drawImage($('#mask')[0], 0, 0);
imgData = ctx.getImageData(0, 0, width, height);
}
Fiddle
What you need to do:
Get the buffer
Get a 32-bits reference of that buffer (If your other pixels are transparent then you can use a Uint32Array buffer to iterate).
Scan 0 - width to find x1 edge
Scan width - 0 to find x2 edge
Scan 0 - height to find y1 edge
Scan height - 0 to find y2 edge
These scans can be combined but for simplicity I'll show each step separately.
Online demo of this can be found here.
Result:
When image is loaded draw it in (if the image is small then the rest of this example would be waste as you would know the coordinates when drawing it - assuming here the image you draw is large with a small image inside it)
(note: this is a non-optimized version for the sake of simplicity)
ctx.drawImage(this, 0, 0, w, h);
var idata = ctx.getImageData(0, 0, w, h), // get image data for canvas
buffer = idata.data, // get buffer (unnes. step)
buffer32 = new Uint32Array(buffer.buffer), // get a 32-bit representation
x, y, // iterators
x1 = w, y1 = h, x2 = 0, y2 = 0; // min/max values
Then scan each edge. For left edge you scan from 0 to width for each line (non optimized):
// get left edge
for(y = 0; y < h; y++) { // line by line
for(x = 0; x < w; x++) { // 0 to width
if (buffer32[x + y * w] > 0) { // non-transparent pixel?
if (x < x1) x1 = x; // if less than current min update
}
}
}
For the right edge you just reverse x iterator:
// get right edge
for(y = 0; y < h; y++) { // line by line
for(x = w; x >= 0; x--) { // from width to 0
if (buffer32[x + y * w] > 0) {
if (x > x2) x2 = x;
}
}
}
And the same is for top and bottom edges just that the iterators are reversed:
// get top edge
for(x = 0; x < w; x++) {
for(y = 0; y < h; y++) {
if (buffer32[x + y * w] > 0) {
if (y < y1) y1 = y;
}
}
}
// get bottom edge
for(x = 0; x < w; x++) {
for(y = h; y >= 0; y--) {
if (buffer32[x + y * w] > 0) {
if (y > y2) y2 = y;
}
}
}
The resulting region is then:
ctx.strokeRect(x1, y1, x2-x1, y2-y1);
There are various optimizations you could implement but they depend entirely on the scenario such as if you know approximate placement then you don't have to iterate all lines/columns.
You could do a brute force guess of he placement by skipping x number of pixels and when you found a non-transparent pixel you could make a max search area based on that and so forth, but that is out of scope here.
Hope this helps!
I was in need of something similar to this, just recently. Although the question is answered, I wanted to post my code for a future reference.
In my case, I'm drawing a (font) icon on a blank/transparent canvas, and want to get the bounding box. Even if I know the height of the icon (using font-size, i.e., height), I can't know the width. So I have to calculate it manually.
I'm not sure if there's a clever way to calculate this. First thing that popped into my head was doing it the hard way: manually checking every pixel, and that's what I did.
I think the code is pretty self-explanatory, so I won't do any explanation. I tried to keep the code as clean as possible.
/* Layer 3: The App */
let canvas = document.querySelector("#canvas");
let input = document.querySelector("#input");
let output = document.querySelector("#output");
canvas.width = 256;
canvas.height = 256;
let context = canvas.getContext("2d");
context.font = "200px Arial, sans-serif";
let drawnLetter = null;
drawLetter(input.value);
function drawLetter(letter) {
letter = letter ? letter[0] : null;
if (!letter) {
// clear canvas
context.clearRect(0, 0, canvas.width, canvas.height);
output.textContent = null;
return;
}
if (letter == drawnLetter) {
return;
}
drawnLetter = letter;
// clear canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// draw letter
context.fillText(letter, 50, canvas.height - 50);
// find edges
let boundingBox = findEdges(context);
// mark the edges
context.beginPath();
context.rect(boundingBox.left, boundingBox.top, boundingBox.width, boundingBox.height);
context.lineWidth = 2;
context.strokeStyle = "red";
context.stroke();
// output the values
output.textContent = JSON.stringify(boundingBox, null, " ");
}
/* Layer 2: Interacting with canvas */
function findEdges(context) {
let left = findLeftEdge(context);
let right = findRightEdge(context);
let top = findTopEdge(context);
let bottom = findBottomEdge(context);
// right and bottom are relative to top left (0,0)
return {
left,
top,
right,
bottom,
width : right - left,
height : bottom - top,
};
}
function findLeftEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let x = 0; x < context.canvas.width; x++) {
for (let y = 0; y < context.canvas.height; y++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return x;
}
}
}
}
function findRightEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let x = context.canvas.width - 1; x >= 0; x--) {
for (let y = 0; y < context.canvas.height; y++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return x;
}
}
}
}
function findTopEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let y = 0; y < context.canvas.height; y++) {
for (let x = 0; x < context.canvas.width; x++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return y;
}
}
}
}
function findBottomEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let y = context.canvas.height - 1; y >= 0; y--) {
for (let x = 0; x < context.canvas.width; x++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return y;
}
}
}
}
/* Layer 1: Interacting with ImageData */
/**
* Returns the pixel array at the specified position.
*/
function getPixel(imageData, x, y) {
return getPixelByIndex(imageData, pos2index(imageData, x, y));
}
/**
* Returns the RGBA values at the specified index.
*/
function getPixelByIndex(imageData, index) {
return [
imageData.data[index + 0],
imageData.data[index + 1],
imageData.data[index + 2],
imageData.data[index + 3],
];
}
/**
* Returns the index of a position.
*/
function pos2index(imageData, x, y) {
return 4 * (y * imageData.width + x);
}
body {
background-color: hsl(0, 0%, 95%);
}
canvas {
background: white;
image-rendering: pixelated;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEXMzMz////TjRV2AAAAEUlEQVQI12P4z8CAFWEX/Q8Afr8P8erzE9cAAAAASUVORK5CYII=);
zoom: 0.8; /* this counters the scale up (125%) of my screen; can be removed */
}
input {
padding: 0.2em;
margin-top: 0.5em;
}
<canvas id="canvas"></canvas>
<br>
<input type="text" id="input" placeholder="type a letter" value="A" onkeyup="drawLetter(this.value)" />
<pre id="output"></pre>

Dynamically adjust text color based on background image

I am working on a product that outputs images from users and the image information is overlayed on top of the aforementioned images. As you might imagine, the images require different text colors due to lightness/darkness. Is there a way to achieve this with JavaScript?
EDIT: I found a similar question to mine and there was a solution given in a jsfiddle (http://jsfiddle.net/xLF38/818). I am using jQuery for my site though. How would I convert the vanilla JavaScript to jQuery?
var rgb = getAverageRGB(document.getElementById('i'));
document.body.style.backgroundColor = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
function getAverageRGB(imgEl) {
var blockSize = 5, // only visit every 5 pixels
defaultRGB = {
r: 0,
g: 0,
b: 0
}, // for non-supporting envs
canvas = document.createElement('canvas'),
context = canvas.getContext && canvas.getContext('2d'),
data, width, height,
i = -4,
length,
rgb = {
r: 0,
g: 0,
b: 0
},
count = 0;
if (!context) {
return defaultRGB;
}
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;
context.drawImage(imgEl, 0, 0);
try {
data = context.getImageData(0, 0, width, height);
} catch (e) {
/* security error, img on diff domain */
alert('x');
return defaultRGB;
}
length = data.data.length;
while ((i += blockSize * 4) < length) {
++count;
rgb.r += data.data[i];
rgb.g += data.data[i + 1];
rgb.b += data.data[i + 2];
}
// ~~ used to floor values
rgb.r = ~~ (rgb.r / count);
rgb.g = ~~ (rgb.g / count);
rgb.b = ~~ (rgb.b / count);
return rgb;
}
I finally found something to do precisely what I want it to do! Enter Brian Gonzalez's
jquery.adaptive-backgrounds.js. Check this out:
$parent.css({
// backgroundColor: data.color
color: data.color
});
I just commented out the backgroundColor rule and made a new one for color. For white text, a text-shadow like:
text-shadow: 0 0 1px rgba($black, 0.3); // using Sass
should be enough. Thank you to everyone for your answers!
This is possible using the canvas element. You would have to create a canvas element, draw the image element into the canvas, get the canvas's image data, look at the portion where the text is, convert those values to grayscale, average them, then compare them with a halfway point. Some example code:
var img = document.getElementById('myImage');
var c = document.createElement('canvas');
var ctx = c.getContext('2d');
var w = img.width, h = img.height;
c.width = w; c.height = h;
ctx.drawImage(img, 0, 0);
var data = ctx.getImageData(0, 0, w, h).data;
var brightness = 0;
var sX = 0, sY = 0, eX = w, eY = h;
var start = (w * sY + sX) * 4, end = (w * eY + eX) * 4;
for (var i = start, n = end; i < n; i += 4) {
var r = data[i],
g = data[i + 1],
b = data[i + 2];
brightness += 0.34 * r + 0.5 * g + 0.16 * b;
if (brightness !== 0) brightness /= 2;
}
if (brightness > 0.5) var textColor = "#FFFFFF";
else var textColor = "#000000";
I haven't tested this code, though it should work. Make sure to change the sX, sY, eX, eY values to only the area where your text is, otherwise you will get unsatisfactory results (it will still work). Good luck!
EDIT:
You will not have to display your image in any special way. Just make sure that the color of the overlay text is the variable textColor.
you could check the background-image attribute with jQuery then adjust the text color dynamically.
var x = $(body).attr("background-image");
switch(x)
{
case "something.png":
// set color here
break;
}

Categories

Resources