Related
The spec has a context.measureText(text) function that will tell you how much width it would require to print that text, but I can't find a way to find out how tall it is. I know it's based on the font, but I don't know to convert a font string to a text height.
Browsers are beginning to support advanced text metrics, which will make this task trivial when it's widely supported:
let metrics = ctx.measureText(text);
let fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
let actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
fontHeight gets you the bounding box height that is constant regardless of the string being rendered. actualHeight is specific to the string being rendered.
Spec: https://www.w3.org/TR/2012/CR-2dcontext-20121217/#dom-textmetrics-fontboundingboxascent and the sections just below it.
Support status (20-Aug-2017):
Chrome has it behind a flag (https://bugs.chromium.org/p/chromium/issues/detail?id=277215).
Firefox has it in development (https://bugzilla.mozilla.org/show_bug.cgi?id=1102584).
Edge has no support (https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/30922861-advanced-canvas-textmetrics).
node-canvas (node.js module), mostly supported (https://github.com/Automattic/node-canvas/wiki/Compatibility-Status).
UPDATE - for an example of this working, I used this technique in the Carota editor.
Following on from ellisbben's answer, here is an enhanced version to get the ascent and descent from the baseline, i.e. same as tmAscent and tmDescent returned by Win32's GetTextMetric API. This is needed if you want to do a word-wrapped run of text with spans in different fonts/sizes.
The above image was generated on a canvas in Safari, red being the top line where the canvas was told to draw the text, green being the baseline and blue being the bottom (so red to blue is the full height).
Using jQuery for succinctness:
var getTextHeight = function(font) {
var text = $('<span>Hg</span>').css({ fontFamily: font });
var block = $('<div style="display: inline-block; width: 1px; height: 0px;"></div>');
var div = $('<div></div>');
div.append(text, block);
var body = $('body');
body.append(div);
try {
var result = {};
block.css({ verticalAlign: 'baseline' });
result.ascent = block.offset().top - text.offset().top;
block.css({ verticalAlign: 'bottom' });
result.height = block.offset().top - text.offset().top;
result.descent = result.height - result.ascent;
} finally {
div.remove();
}
return result;
};
In addition to a text element, I add a div with display: inline-block so I can set its vertical-align style, and then find out where the browser has put it.
So you get back an object with ascent, descent and height (which is just ascent + descent for convenience). To test it, it's worth having a function that draws a horizontal line:
var testLine = function(ctx, x, y, len, style) {
ctx.strokeStyle = style;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + len, y);
ctx.closePath();
ctx.stroke();
};
Then you can see how the text is positioned on the canvas relative to the top, baseline and bottom:
var font = '36pt Times';
var message = 'Big Text';
ctx.fillStyle = 'black';
ctx.textAlign = 'left';
ctx.textBaseline = 'top'; // important!
ctx.font = font;
ctx.fillText(message, x, y);
// Canvas can tell us the width
var w = ctx.measureText(message).width;
// New function gets the other info we need
var h = getTextHeight(font);
testLine(ctx, x, y, w, 'red');
testLine(ctx, x, y + h.ascent, w, 'green');
testLine(ctx, x, y + h.height, w, 'blue');
You can get a very close approximation of the vertical height by checking the length of a capital M.
ctx.font = 'bold 10px Arial';
lineHeight = ctx.measureText('M').width;
The canvas spec doesn't give us a method for measuring the height of a string. However, you can set the size of your text in pixels and you can usually figure out what the vertical bounds are relatively easily.
If you need something more precise then you could throw text onto the canvas and then get pixel data and figure out how many pixels are used vertically. This would be relatively simple, but not very efficient. You could do something like this (it works, but draws some text onto your canvas that you would want to remove):
function measureTextHeight(ctx, left, top, width, height) {
// Draw the text in the specified area
ctx.save();
ctx.translate(left, top + Math.round(height * 0.8));
ctx.mozDrawText('gM'); // This seems like tall text... Doesn't it?
ctx.restore();
// Get the pixel data from the canvas
var data = ctx.getImageData(left, top, width, height).data,
first = false,
last = false,
r = height,
c = 0;
// Find the last line with a non-white pixel
while(!last && r) {
r--;
for(c = 0; c < width; c++) {
if(data[r * width * 4 + c * 4 + 3]) {
last = r;
break;
}
}
}
// Find the first line with a non-white pixel
while(r) {
r--;
for(c = 0; c < width; c++) {
if(data[r * width * 4 + c * 4 + 3]) {
first = r;
break;
}
}
// If we've got it then return the height
if(first != r) return last - first;
}
// We screwed something up... What do you expect from free code?
return 0;
}
// Set the font
context.mozTextStyle = '32px Arial';
// Specify a context and a rect that is safe to draw in when calling measureTextHeight
var height = measureTextHeight(context, 0, 0, 50, 50);
console.log(height);
For Bespin they do fake a height by measuring the width of a lowercase 'm'... I don't know how this is used, and I would not recommend this method. Here is the relevant Bespin method:
var fixCanvas = function(ctx) {
// upgrade Firefox 3.0.x text rendering to HTML 5 standard
if (!ctx.fillText && ctx.mozDrawText) {
ctx.fillText = function(textToDraw, x, y, maxWidth) {
ctx.translate(x, y);
ctx.mozTextStyle = ctx.font;
ctx.mozDrawText(textToDraw);
ctx.translate(-x, -y);
}
}
if (!ctx.measureText && ctx.mozMeasureText) {
ctx.measureText = function(text) {
ctx.mozTextStyle = ctx.font;
var width = ctx.mozMeasureText(text);
return { width: width };
}
}
if (ctx.measureText && !ctx.html5MeasureText) {
ctx.html5MeasureText = ctx.measureText;
ctx.measureText = function(text) {
var textMetrics = ctx.html5MeasureText(text);
// fake it 'til you make it
textMetrics.ascent = ctx.html5MeasureText("m").width;
return textMetrics;
}
}
// for other browsers
if (!ctx.fillText) {
ctx.fillText = function() {}
}
if (!ctx.measureText) {
ctx.measureText = function() { return 10; }
}
};
EDIT: Are you using canvas transforms? If so, you'll have to track the transformation matrix. The following method should measure the height of text with the initial transform.
EDIT #2: Oddly the code below does not produce correct answers when I run it on this StackOverflow page; it's entirely possible that the presence of some style rules could break this function.
The canvas uses fonts as defined by CSS, so in theory we can just add an appropriately styled chunk of text to the document and measure its height. I think this is significantly easier than rendering text and then checking pixel data and it should also respect ascenders and descenders. Check out the following:
var determineFontHeight = function(fontStyle) {
var body = document.getElementsByTagName("body")[0];
var dummy = document.createElement("div");
var dummyText = document.createTextNode("M");
dummy.appendChild(dummyText);
dummy.setAttribute("style", fontStyle);
body.appendChild(dummy);
var result = dummy.offsetHeight;
body.removeChild(dummy);
return result;
};
//A little test...
var exampleFamilies = ["Helvetica", "Verdana", "Times New Roman", "Courier New"];
var exampleSizes = [8, 10, 12, 16, 24, 36, 48, 96];
for(var i = 0; i < exampleFamilies.length; i++) {
var family = exampleFamilies[i];
for(var j = 0; j < exampleSizes.length; j++) {
var size = exampleSizes[j] + "pt";
var style = "font-family: " + family + "; font-size: " + size + ";";
var pixelHeight = determineFontHeight(style);
console.log(family + " " + size + " ==> " + pixelHeight + " pixels high.");
}
}
You'll have to make sure you get the font style correct on the DOM element that you measure the height of but that's pretty straightforward; really you should use something like
var canvas = /* ... */
var context = canvas.getContext("2d");
var canvasFont = " ... ";
var fontHeight = determineFontHeight("font: " + canvasFont + ";");
context.font = canvasFont;
/*
do your stuff with your font and its height here.
*/
As JJ Stiff suggests, you can add your text to a span and then measure the offsetHeight of the span.
var d = document.createElement("span");
d.font = "20px arial";
d.textContent = "Hello world!";
document.body.appendChild(d);
var emHeight = d.offsetHeight;
document.body.removeChild(d);
As shown on HTML5Rocks
Isn't the height of the text in pixels equal to the font size (in pts) if you define the font using context.font ?
Just to add to Daniel's answer (which is great! and absolutely right!), version without JQuery:
function objOff(obj)
{
var currleft = currtop = 0;
if( obj.offsetParent )
{ do { currleft += obj.offsetLeft; currtop += obj.offsetTop; }
while( obj = obj.offsetParent ); }
else { currleft += obj.offsetLeft; currtop += obj.offsetTop; }
return [currleft,currtop];
}
function FontMetric(fontName,fontSize)
{
var text = document.createElement("span");
text.style.fontFamily = fontName;
text.style.fontSize = fontSize + "px";
text.innerHTML = "ABCjgq|";
// if you will use some weird fonts, like handwriting or symbols, then you need to edit this test string for chars that will have most extreme accend/descend values
var block = document.createElement("div");
block.style.display = "inline-block";
block.style.width = "1px";
block.style.height = "0px";
var div = document.createElement("div");
div.appendChild(text);
div.appendChild(block);
// this test div must be visible otherwise offsetLeft/offsetTop will return 0
// but still let's try to avoid any potential glitches in various browsers
// by making it's height 0px, and overflow hidden
div.style.height = "0px";
div.style.overflow = "hidden";
// I tried without adding it to body - won't work. So we gotta do this one.
document.body.appendChild(div);
block.style.verticalAlign = "baseline";
var bp = objOff(block);
var tp = objOff(text);
var taccent = bp[1] - tp[1];
block.style.verticalAlign = "bottom";
bp = objOff(block);
tp = objOff(text);
var theight = bp[1] - tp[1];
var tdescent = theight - taccent;
// now take it off :-)
document.body.removeChild(div);
// return text accent, descent and total height
return [taccent,theight,tdescent];
}
I've just tested the code above and works great on latest Chrome, FF and Safari on Mac.
EDIT: I have added font size as well and tested with webfont instead of system font - works awesome.
I solved this problem straitforward - using pixel manipulation.
Here is graphical answer:
Here is code:
function textHeight (text, font) {
var fontDraw = document.createElement("canvas");
var height = 100;
var width = 100;
// here we expect that font size will be less canvas geometry
fontDraw.setAttribute("height", height);
fontDraw.setAttribute("width", width);
var ctx = fontDraw.getContext('2d');
// black is default
ctx.fillRect(0, 0, width, height);
ctx.textBaseline = 'top';
ctx.fillStyle = 'white';
ctx.font = font;
ctx.fillText(text/*'Eg'*/, 0, 0);
var pixels = ctx.getImageData(0, 0, width, height).data;
// row numbers where we first find letter end where it ends
var start = -1;
var end = -1;
for (var row = 0; row < height; row++) {
for (var column = 0; column < width; column++) {
var index = (row * width + column) * 4;
// if pixel is not white (background color)
if (pixels[index] == 0) {
// we havent met white (font color) pixel
// on the row and the letters was detected
if (column == width - 1 && start != -1) {
end = row;
row = height;
break;
}
continue;
}
else {
// we find top of letter
if (start == -1) {
start = row;
}
// ..letters body
break;
}
}
}
/*
document.body.appendChild(fontDraw);
fontDraw.style.pixelLeft = 400;
fontDraw.style.pixelTop = 400;
fontDraw.style.position = "absolute";
*/
return end - start;
}
one line answer
var height = parseInt(ctx.font) * 1.2;
CSS "line-height: normal" is between 1 and 1.2
read here for more info
I'm kind of shocked that there are no correct answers here. There is no need to make an estimate or a guess. Also, the font-size is not the actual size of the bounding box of the font. The font height depends on whether you have ascenders and descenders.
To calculate it, use ctx.measureText() and add together the actualBoundingBoxAscent and the actualBoundingBoxDescent. That'll give you the actual size. You can also add together the font* versions to get the size that is used to calculate things like element height, but isn't strictly the height of the actual used space for the font.
const text = 'Hello World';
const canvas = document.querySelector('canvas');
canvas.width = 500;
canvas.height = 200;
const ctx = canvas.getContext('2d');
const fontSize = 100;
ctx.font = `${fontSize}px Arial, Helvetica, sans-serif`;
// top is critical to the fillText() calculation
// you can use other positions, but you need to adjust the calculation
ctx.textBaseline = 'top';
ctx.textAlign = 'center';
const metrics = ctx.measureText(text);
const width = metrics.width;
const actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
// fallback to using fontSize if fontBoundingBoxAscent isn't available, like in Firefox. Should be close enough that you aren't more than a pixel off in most cases.
const fontHeight = (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) ?? fontSize;
ctx.fillStyle = '#00F'; // blue
ctx.fillRect((canvas.width / 2) - (width / 2), (canvas.height / 2) - (fontHeight / 2), width, fontHeight);
ctx.fillStyle = '#0F0'; // green
ctx.fillRect((canvas.width / 2) - (width / 2), (canvas.height / 2) - (actualHeight / 2), width, actualHeight);
// canvas.height / 2 - actualHeight / 2 gets you to the top of
// the green box. You have to add actualBoundingBoxAscent to shift
// it just right
ctx.fillStyle = '#F00'; // red
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - actualHeight / 2 + metrics.actualBoundingBoxAscent);
<canvas></canvas>
This is what I did based on some of the other answers here:
function measureText(text, font) {
const span = document.createElement('span');
span.appendChild(document.createTextNode(text));
Object.assign(span.style, {
font: font,
margin: '0',
padding: '0',
border: '0',
whiteSpace: 'nowrap'
});
document.body.appendChild(span);
const {width, height} = span.getBoundingClientRect();
span.remove();
return {width, height};
}
var font = "italic 100px Georgia";
var text = "abc this is a test";
console.log(measureText(text, font));
I'm writing a terminal emulator so I needed to draw rectangles around characters.
var size = 10
var lineHeight = 1.2 // CSS "line-height: normal" is between 1 and 1.2
context.font = size+'px/'+lineHeight+'em monospace'
width = context.measureText('m').width
height = size * lineHeight
Obviously if you want the exact amount of space the character takes up, it won't help. But it'll give you a good approximation for certain uses.
I have implemented a nice library for measuring the exact height and width of text using HTML canvas. This should do what you want.
https://github.com/ChrisBellew/text-measurer.js
Here is a simple function. No library needed.
I wrote this function to get the top and bottom bounds relative to baseline. If textBaseline is set to alphabetic. What it does is it creates another canvas, and then draws there, and then finds the top most and bottom most non blank pixel. And that is the top and bottom bounds. It returns it as relative, so if height is 20px, and there is nothing below the baseline, then the top bound is -20.
You must supply characters to it. Otherwise it will give you 0 height and 0 width, obviously.
Usage:
alert(measureHeight('40px serif', 40, 'rg').height)
Here is the function:
function measureHeight(aFont, aSize, aChars, aOptions={}) {
// if you do pass aOptions.ctx, keep in mind that the ctx properties will be changed and not set back. so you should have a devoted canvas for this
// if you dont pass in a width to aOptions, it will return it to you in the return object
// the returned width is Math.ceil'ed
console.error('aChars: "' + aChars + '"');
var defaultOptions = {
width: undefined, // if you specify a width then i wont have to use measureText to get the width
canAndCtx: undefined, // set it to object {can:,ctx:} // if not provided, i will make one
range: 3
};
aOptions.range = aOptions.range || 3; // multiples the aSize by this much
if (aChars === '') {
// no characters, so obviously everything is 0
return {
relativeBot: 0,
relativeTop: 0,
height: 0,
width: 0
};
// otherwise i will get IndexSizeError: Index or size is negative or greater than the allowed amount error somewhere below
}
// validateOptionsObj(aOptions, defaultOptions); // not needed because all defaults are undefined
var can;
var ctx;
if (!aOptions.canAndCtx) {
can = document.createElement('canvas');;
can.mozOpaque = 'true'; // improved performanceo on firefox i guess
ctx = can.getContext('2d');
// can.style.position = 'absolute';
// can.style.zIndex = 10000;
// can.style.left = 0;
// can.style.top = 0;
// document.body.appendChild(can);
} else {
can = aOptions.canAndCtx.can;
ctx = aOptions.canAndCtx.ctx;
}
var w = aOptions.width;
if (!w) {
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.font = aFont;
w = ctx.measureText(aChars).width;
}
w = Math.ceil(w); // needed as i use w in the calc for the loop, it needs to be a whole number
// must set width/height, as it wont paint outside of the bounds
can.width = w;
can.height = aSize * aOptions.range;
ctx.font = aFont; // need to set the .font again, because after changing width/height it makes it forget for some reason
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.fillStyle = 'white';
console.log('w:', w);
var avgOfRange = (aOptions.range + 1) / 2;
var yBaseline = Math.ceil(aSize * avgOfRange);
console.log('yBaseline:', yBaseline);
ctx.fillText(aChars, 0, yBaseline);
var yEnd = aSize * aOptions.range;
var data = ctx.getImageData(0, 0, w, yEnd).data;
// console.log('data:', data)
var botBound = -1;
var topBound = -1;
// measureHeightY:
for (y=0; y<=yEnd; y++) {
for (var x = 0; x < w; x += 1) {
var n = 4 * (w * y + x);
var r = data[n];
var g = data[n + 1];
var b = data[n + 2];
// var a = data[n + 3];
if (r+g+b > 0) { // non black px found
if (topBound == -1) {
topBound = y;
}
botBound = y; // break measureHeightY; // dont break measureHeightY ever, keep going, we till yEnd. so we get proper height for strings like "`." or ":" or "!"
break;
}
}
}
return {
relativeBot: botBound - yBaseline, // relative to baseline of 0 // bottom most row having non-black
relativeTop: topBound - yBaseline, // relative to baseline of 0 // top most row having non-black
height: (botBound - topBound) + 1,
width: w// EDIT: comma has been added to fix old broken code.
};
}
relativeBot, relativeTop, and height are the useful things in the return object.
Here is example usage:
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<script>
function measureHeight(aFont, aSize, aChars, aOptions={}) {
// if you do pass aOptions.ctx, keep in mind that the ctx properties will be changed and not set back. so you should have a devoted canvas for this
// if you dont pass in a width to aOptions, it will return it to you in the return object
// the returned width is Math.ceil'ed
console.error('aChars: "' + aChars + '"');
var defaultOptions = {
width: undefined, // if you specify a width then i wont have to use measureText to get the width
canAndCtx: undefined, // set it to object {can:,ctx:} // if not provided, i will make one
range: 3
};
aOptions.range = aOptions.range || 3; // multiples the aSize by this much
if (aChars === '') {
// no characters, so obviously everything is 0
return {
relativeBot: 0,
relativeTop: 0,
height: 0,
width: 0
};
// otherwise i will get IndexSizeError: Index or size is negative or greater than the allowed amount error somewhere below
}
// validateOptionsObj(aOptions, defaultOptions); // not needed because all defaults are undefined
var can;
var ctx;
if (!aOptions.canAndCtx) {
can = document.createElement('canvas');;
can.mozOpaque = 'true'; // improved performanceo on firefox i guess
ctx = can.getContext('2d');
// can.style.position = 'absolute';
// can.style.zIndex = 10000;
// can.style.left = 0;
// can.style.top = 0;
// document.body.appendChild(can);
} else {
can = aOptions.canAndCtx.can;
ctx = aOptions.canAndCtx.ctx;
}
var w = aOptions.width;
if (!w) {
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.font = aFont;
w = ctx.measureText(aChars).width;
}
w = Math.ceil(w); // needed as i use w in the calc for the loop, it needs to be a whole number
// must set width/height, as it wont paint outside of the bounds
can.width = w;
can.height = aSize * aOptions.range;
ctx.font = aFont; // need to set the .font again, because after changing width/height it makes it forget for some reason
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.fillStyle = 'white';
console.log('w:', w);
var avgOfRange = (aOptions.range + 1) / 2;
var yBaseline = Math.ceil(aSize * avgOfRange);
console.log('yBaseline:', yBaseline);
ctx.fillText(aChars, 0, yBaseline);
var yEnd = aSize * aOptions.range;
var data = ctx.getImageData(0, 0, w, yEnd).data;
// console.log('data:', data)
var botBound = -1;
var topBound = -1;
// measureHeightY:
for (y=0; y<=yEnd; y++) {
for (var x = 0; x < w; x += 1) {
var n = 4 * (w * y + x);
var r = data[n];
var g = data[n + 1];
var b = data[n + 2];
// var a = data[n + 3];
if (r+g+b > 0) { // non black px found
if (topBound == -1) {
topBound = y;
}
botBound = y; // break measureHeightY; // dont break measureHeightY ever, keep going, we till yEnd. so we get proper height for strings like "`." or ":" or "!"
break;
}
}
}
return {
relativeBot: botBound - yBaseline, // relative to baseline of 0 // bottom most row having non-black
relativeTop: topBound - yBaseline, // relative to baseline of 0 // top most row having non-black
height: (botBound - topBound) + 1,
width: w
};
}
</script>
</head>
<body style="background-color:steelblue;">
<input type="button" value="reuse can" onClick="alert(measureHeight('40px serif', 40, 'rg', {canAndCtx:{can:document.getElementById('can'), ctx:document.getElementById('can').getContext('2d')}}).height)">
<input type="button" value="dont reuse can" onClick="alert(measureHeight('40px serif', 40, 'rg').height)">
<canvas id="can"></canvas>
<h1>This is a Heading</h1>
<p>This is a paragraph.</p>
</body>
</html>
The relativeBot and relativeTop are what you see in this image here:
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text
Funny that TextMetrics has width only and no height:
http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#textmetrics
Can you use a Span as on this example?
http://mudcu.be/journal/2011/01/html5-typographic-metrics/#alignFix
First of all, you need to set the height of a font size, and then according to the value of the font height to determine the current height of your text is how much, cross-text lines, of course, the same height of the font need to accumulate, if the text does not exceed the largest text box Height, all show, otherwise, only show the text within the box text. High values need your own definition. The larger the preset height, the greater the height of the text that needs to be displayed and intercepted.
After the effect is processed(solve)
Before the effect is processed(
unsolved)
AutoWrappedText.auto_wrap = function(ctx, text, maxWidth, maxHeight) {
var words = text.split("");
var lines = [];
var currentLine = words[0];
var total_height = 0;
for (var i = 1; i < words.length; i++) {
var word = words[i];
var width = ctx.measureText(currentLine + word).width;
if (width < maxWidth) {
currentLine += word;
} else {
lines.push(currentLine);
currentLine = word;
// TODO dynamically get font size
total_height += 25;
if (total_height >= maxHeight) {
break
}
}
}
if (total_height + 25 < maxHeight) {
lines.push(currentLine);
} else {
lines[lines.length - 1] += "…";
}
return lines;};
I found that JUST FOR ARIAL the simplest, fastest and accuratest way to find height of bounding box is to use the width of certain letters. If you plan to use a certain font without letting user to choose one different, you can do a little research to find the right letter that do the job for that font.
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="700" height="200" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = "100px Arial";
var txt = "Hello guys!"
var Hsup=ctx.measureText("H").width;
var Hbox=ctx.measureText("W").width;
var W=ctx.measureText(txt).width;
var W2=ctx.measureText(txt.substr(0, 9)).width;
ctx.fillText(txt, 10, 100);
ctx.rect(10,100, W, -Hsup);
ctx.rect(10,100+Hbox-Hsup, W2, -Hbox);
ctx.stroke();
</script>
<p><strong>Note:</strong> The canvas tag is not supported in Internet
Explorer 8 and earlier versions.</p>
</body>
</html>
setting the font size might not be practical though, since setting
ctx.font = ''
will use the one defined by CSS as well as any embedded font tags. If you use the CSS font you have no idea what the height is from a programmatic way, using the measureText method, which is very short sighted. On another note though, IE8 DOES return the width and height.
This works 1) for multiline text as well 2) and even in IE9!
<div class="measureText" id="measureText">
</div>
.measureText {
margin: 0;
padding: 0;
border: 0;
font-family: Arial;
position: fixed;
visibility: hidden;
height: auto;
width: auto;
white-space: pre-wrap;
line-height: 100%;
}
function getTextFieldMeasure(fontSize, value) {
const div = document.getElementById("measureText");
// returns wrong result for multiline text with last line empty
let arr = value.split('\n');
if (arr[arr.length-1].length == 0) {
value += '.';
}
div.innerText = value;
div.style['font-size']= fontSize + "px";
let rect = div.getBoundingClientRect();
return {width: rect.width, height: rect.height};
};
I know this is an old answered question, but for future reference I'd like to add a short, minimal, JS-only (no jquery) solution I believe people can benefit from:
var measureTextHeight = function(fontFamily, fontSize)
{
var text = document.createElement('span');
text.style.fontFamily = fontFamily;
text.style.fontSize = fontSize + "px";
text.textContent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ";
document.body.appendChild(text);
var result = text.getBoundingClientRect().height;
document.body.removeChild(text);
return result;
};
I monkey patched CanvasRenderingContext2D.measureText() in one of my project to include actual height of the text. It's written in vanilla JS and has zero dependencies.
/*
* Monkeypatch CanvasRenderingContext2D.measureText() to include actual height of the text
*/
; (function (global) {
"use strict";
var _measureText = global.CanvasRenderingContext2D.prototype.measureText;
global.CanvasRenderingContext2D.prototype.measureText = function () {
var textMetrics = _measureText.apply(this, arguments);
var _getHeight = function (text) {
var $span = global.document.createElement("span");
var spanTextNode = global.document.createTextNode(text);
$span.appendChild(spanTextNode);
$span.setAttribute("style", `font: ${this.font}`);
var $div = global.document.createElement("div");
$div.setAttribute("style", "display: inline-block; width: 1px; height: 0; vertical-align: super;");
var $parentDiv = global.document.createElement("div");
$parentDiv.appendChild($span);
$parentDiv.appendChild($div);
var $body = global.document.getElementsByTagName("body")[0];
$body.appendChild($parentDiv);
var divRect = $div.getBoundingClientRect();
var spanRect = $span.getBoundingClientRect();
var result = {};
$div.style.verticalAlign = "baseline";
result.ascent = divRect.top - spanRect.top;
$div.style.verticalAlign = "bottom";
result.height = divRect.top - spanRect.top;
result.descent = result.height - result.ascent;
$body.removeChild($parentDiv);
return result.height - result.descent;
}.bind(this);
var height = _getHeight(arguments[0]);
global.Object.defineProperty(textMetrics, "height", { value: height });
return textMetrics;
};
})(window);
You can use it like this
ctx.font = "bold 64px Verdana, sans-serif"; // Automatically considers it as part of height calculation
var textMetrics = ctx.measureText("Foobar");
var textHeight = textMetrics.height;
parseInt(ctx.font, 10)
e.g.
let text_height = parseInt(ctx.font, 10)
e.g. returns 35
In normal situations the following should work:
var can = CanvasElement.getContext('2d'); //get context
var lineHeight = /[0-9]+(?=pt|px)/.exec(can.font); //get height from font variable
This is madding... The height of the text is the font size.. Didn't any of you read the documentation?
context.font = "22px arial";
this will set the height to 22px.
the only reason there is a..
context.measureText(string).width
is because that the width of the string can not be determined unless it knows the string you want the width of but for all the strings drawn with the font.. the height will be 22px.
if you use another measurement than px then the height will still be the same but with that measurement so at most all you would have to do is convert the measurement.
Approximate solution:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = "100px Arial";
var txt = "Hello guys!"
var wt = ctx.measureText(txt).width;
var height = wt / txt.length;
This will be accurate result in monospaced font.
Before you go off saying I'm crazy, believe me, I know. I'm not going for a website that renders fast, loads fast, or gets a high lighthouse score. I just want it to work.
I have some javascript that picks up all the pixel colors of an image. With this function, I create a div element that is 1px by 1px and set the background color to the pixel color of those same coordinates. Then the coordinates are used to set the top and left values. My code does what it's told.
Here's my problem, my image is 700px by 387px. If you do the math, that works out to 270,900 html elements. Chrome, simply isn't built for this madness. I want to see this work, I want to "manually" create an image with div elements, somehow. My cpu maxes out when I try to do so, and I'm sure I'd run out of ram eventually.
Everything works fine if I only try hundreds or a few thousand pixels, but any more, and chrome dies. I'm not sure if it's calculating in the browser that may be my problem, or if chrome cant display this many elements, or both. I suppose I could do the same math on my server with python, and append it to html, but then chrome probably couldn't display it.
Obviously, this isn't super important, just fun. I think the community will enjoy the challenge as well.
Here's calculating 100 pixels:
onload = e => {
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function img(x, y) {
var img = document.getElementById('my-image');
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var pixelData = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
}
//x = 700 y = 387
for (var x = 0; x < 10; x++) {
for (var y = 0; y < 10; y++) {
document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
}
}
};
div {
position: absolute;
width: 1px;
height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">
here's calculating 2500px (still works, takes a while)
onload = e => {
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function img(x, y) {
var img = document.getElementById('my-image');
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var pixelData = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
}
//x = 700 y = 387
for (var x = 0; x < 50; x++) {
for (var y = 0; y < 50; y++) {
document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
}
}
};
div {
position: absolute;
width: 1px;
height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">
Cheers, Isaac.
At the moment, you are doing the following for every pixel
Create canvas
get the context and draw to the image
get the context and get the pixel data for one pixel
create a DIV
add it to the DOM
Now, lets streamline this
The following can be done ONCE
create the canvas
get the context
use the context to draw the image
use the context to get the image data
create an empty string
Now, for each pixel
get the pixel data
add the html for the div to the string
And finally, just ONCE
Add the string with all the divs to the DOM
Something like:
const componentToHex = c => c.toString(16).padStart(2, '0');
const rgbToHex = (r, g, b) => `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`;
const img = document.getElementById('my-image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const w = canvas.width = img.width;
const h = canvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
const imageData = context.getImageData(0, 0, w, h).data;
const pixel = (x, y) => {
const index = (w * y + x) * 4;
return rgbToHex(imageData[index], imageData[index + 1], imageData[index + 2]);
}
const df = document.createDocumentFragment();
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const div = df.appendChild(document.createElement('div'));
div.style.top = y + "px";
div.style.left = x + "px";
div.style.backgroundColor = pixel(x, y);
}
}
document.body.appendChild(df);
Note: it may not be the case now, but such a loop may work faster inside a function - often loops in the global context are slower
So, you could wrap the whole code above in
(() => {
// the code from above
})();
And see significant improvement again - not sure, used to be the case in years gone by
changed to use document fragment for a further 25% speed improvement
Now takes 1.4 seconds in firefox for a 640x480 image, 2.3 seconds in chrome - which didn't really see a big difference between using insertAdjacentHTML vs a document fragment
another thing to note. In Firefox the page becomes sluggish, in chrome, for 640x480, no such issue
So I am fooling around with pixel manipulation in canvas. Right now I have code that allows you to draw to canvas. Then, when you have something drawn, there is a button you can press to manipulate the pixels, translating them either one tile to the right or one tile to the left, alternating every other row. The code looks something like this:
First, pushing the button will start a function that creates two empty arrays where the pixel data is going to go. Then it goes through the pixels, row by row, making each row it's own array. All the row arrays are added into one array of all the pixels data.
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixels);
pixels = [];
};
Continuing in the function, next it clears the canvas and shifts the arrays every other either going one to the right or one to the left.
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < rowArray.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
Last part of the function now takes the shifted lists of pixel data and distributes them back onto the canvas.
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
As of right now, it works fine. No data is lost and pixels are shifted correctly. What I am wondering if possible, is there a way to do this that is more efficient? Right now, doing this pixel by pixel takes a long time so I have to go by 20x20 px tiles or higher to have realistic load times. This is my first attempt at pixel manipulation so there is probably quite a few things I'm unaware of. It could be my laptop is not powerful enough. Also, I've noticed that sometimes running this function multiple times in a row will significantly reduce load times. Any help or suggestions are much appreciated!
Full function :
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixel);
pixels = [];
};
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < pixelsListList.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
Performance pixel manipulation.
The given answer is so bad that I have to post a better solution.
And with that a bit of advice when it comes to performance critical code. Functional programming has no place in code that requires the best performance possible.
The most basic pixel manipulation.
The example does the same as the other answer. It uses a callback to select the processing and provides a set of functions to create, filter, and set the pixel data.
Because images can be very large 2Megp plus the filter is timed to check performance. The number of pixels, time taken in µs (1/1,000,000th second), pixels per µs and pixels per second. For realtime processing of a HD 1920*1080 you need a rate of ~125,000,000 pixels per second (60fps).
NOTE babel has been turned off to ensure code is run as is. Sorry IE11 users time to upgrade don`t you think?
canvas.addEventListener('click', ()=>{
var time = performance.now();
ctx.putImageData(processPixels(randomPixels,invertPixels), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "pix per µs "+pps+" pixel per second";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
const randomPixels = putPixels(ctx,createImageData(canvas.width, canvas.height, randomRGB));
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Why functional programming is bad for pixel processing.
To compare below is a timed version of George Campbell Answer that uses functional programming paradigms. The rate will depend on the device and browser but is 2 orders of magnitude slower.
Also if you click, repeating the invert function many times you will notice the GC lags that make functional programming such a bad choice for performance code.
The standard method (first snippet) does not suffer from GC lag because it barely uses any memory apart from the original pixel buffer.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const pixelCount = innerWidth * innerHeight;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
return Array.from(pixels.data).map((pixel, i) => {
//red or green or blue or alpha
let newValue = cb({r: i%4 === 0, g:i%4 === 1, b:i%4 === 2, a:i%4 === 3, value: pixel});
if(typeof newValue === 'undefined' || newValue === null){
throw new Error("undefined/null pixel value "+typeof newValue+" "+newValue);
}
return newValue;
});
}
//callback to apply to each pixel (randomize)
function randomRGB({a}){
if(a){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB({a, value}){
if(a){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
var time = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
if(rate < 1){
rate = "less than 1";
}
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time|0).toLocaleString() + "µs, "+ rate + "pix per µs "+pps+" pixel per second";
});
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">George Campbell Answer. Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Some more pixel processing
The next sample demonstrates some basic pixel manipulation.
Random. Totaly random pixels
Invert. Inverts the pixel colors
B/W. Converts to simple black and white (not perceptual B/W)
Noise. Adds strong noise to pixels. Will reduce total brightness.
2 Bit. Pixel channel data is reduced to 2 bits per RGB.
Blur. Most basic blur function requires a copy of the pixel data to work and is thus expensive in terms of memory and processing overheads. But as NONE of the canvas/SVG filters do the correct logarithmic filter this is the only way to get a good quality blur for the 2D canvas. Unfortunately it is rather slow.
Channel Shift. Moves channels blue to red, red to green, green to blue
Shuffle pixels. Randomly shuffles pixels with one of its neighbours.
For larger images. To prevent filters from blocking the page you would move the imageData to a worker and process the pixels there.
document.body.addEventListener('click', (e)=>{
if(e.target.type !== "button" || e.target.dataset.filter === "test"){
testPattern();
pixels = getImageData(ctx);
info.textContent = "Untimed content render."
return;
}
var time = performance.now();
ctx.putImageData(processPixels(pixels,pixelFilters[e.target.dataset.filter]), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Filter "+e.target.value+ " " +(e.target.dataset.note ? e.target.dataset.note : "") + pixelCount.toLocaleString() + "px : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "px per µs "+pps+" pps";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
var min = Math.min(innerWidth,innerHeight) * 0.45;
function testPattern(){
var grad = ctx.createLinearGradient(0,0,0,canvas.height);
grad.addColorStop(0,"#000");
grad.addColorStop(0.5,"#FFF");
grad.addColorStop(1,"#000");
ctx.fillStyle = grad;
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);
"000,AAA,FFF,F00,00F,A00,00A,FF0,0FF,AA0,0AA,0F0,F0F,0A0,A0A".split(",").forEach((col,i) => {
circle("#"+col, min * (1-i/16));
});
}
function circle(col,size){
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, size, 0 , Math.PI * 2);
ctx.fill();
}
testPattern();
var pixels = getImageData(ctx);
function getImageData(ctx, x = 0, y = 0,width = ctx.canvas.width, height = ctx.canvas.height){
return ctx.getImageData(x,y,width, height);
}
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function randomNoise(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function twoBit(pixelData) {
var i = 0;
var dat = pixelData.data;
var scale = 255 / 196;
while (i < dat.length) {
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
i ++; // skip alpha
}
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function simpleBW(pixelData) {
var bw,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
bw = (dat[i] + dat[i+1] + dat[i+2]) / 3;
dat[i++] = bw;
dat[i++] = bw;
dat[i++] = bw;
i ++; // skip alpha
}
return pixelData;
}
function simpleBlur(pixelData) {
var i = 0;
var dat = pixelData.data;
var buf = new Uint8Array(dat.length);
buf.set(dat);
var w = pixelData.width * 4;
i += w;
while (i < dat.length - w) {
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
i ++; // skip alpha
}
return pixelData;
}
function channelShift(pixelData) {
var r,g,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
r = dat[i];
g = dat[i+1];
dat[i] = dat[i+2];
dat[i+1] = r;
dat[i+2] = g;
i += 4;
}
return pixelData;
}
function pixelShuffle(pixelData) {
var r,g,b,n,rr,gg,bb,i = 0;
var dat = pixelData.data;
var next = [-pixelData.width*4,pixelData.width*4,-4,4];
var len = dat.length;
while (i < dat.length) {
n = (i + next[Math.random() * 4 | 0]) % len;
r = dat[i];
g = dat[i+1];
b = dat[i+2];
dat[i] = dat[n];
dat[i+1] = dat[n + 1];
dat[i+2] = dat[n + 2];
dat[n] = r;
dat[n+1] = g;
dat[n+2] = b;
i += 4;
}
return pixelData;
}
const pixelFilters = {
randomRGB,
invertPixels,
simpleBW,
randomNoise,
twoBit,
simpleBlur,
channelShift,
pixelShuffle,
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
}
.m {
top : 30px;
z-index : 20;
}
#info {
z-index : 10;
background : rgba(255,255,255,0.75);
}
<canvas class="abs" id="canvas"></canvas>
<div class="abs" id="buttons">
<input type ="button" data-filter = "randomRGB" value ="Random"/>
<input type ="button" data-filter = "invertPixels" value ="Invert"/>
<input type ="button" data-filter = "simpleBW" value ="B/W"/>
<input type ="button" data-filter = "randomNoise" value ="Noise"/>
<input type ="button" data-filter = "twoBit" value ="2 Bit" title = "pixel channel data is reduced to 2 bits per RGB"/>
<input type ="button" data-note="High quality blur using logarithmic channel values. " data-filter = "simpleBlur" value ="Blur" title = "Blur requires a copy of pixel data"/>
<input type ="button" data-filter = "channelShift" value ="Ch Shift" title = "Moves channels blue to red, red to green, green to blue"/>
<input type ="button" data-filter = "pixelShuffle" value ="Shuffle" title = "randomly shuffles pixels with one of its neighbours"/>
<input type ="button" data-filter = "test" value ="Test Pattern"/>
</div>
<div class="abs m" id="info"></div>
It makes more sense to use something like ctx.getImageData or .createImageData only once per image, not for each pixel.
You can loop the ImageData.data "array-like" Uint8ClampedArray. Each 4 items in the array represent a single pixel, these being red, green, blue, and alpha parts of the pixel. Each can be an integer between 0 and 255, where [0,0,0,0,255,255,255,255,...] means the first pixel is transparent (and black?), and the second pixel is white and full opacity.
here is something I just made, not benchmarked but likely more efficient.
It creates image data, and you can edit image data by passing in a function to the edit image data function, the callback function is called for each pixel in an image data and returns an object containing value (between 0 and 255), and booleans for r, g, b.
For example for invert you can return 255-value.
this example starts with random pixels, clicking them will apply the invertRGB function to it.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
let i = 0;
let len = pixels.data.length;
let outputPixels = [];
for(i=0;i<len;i++){
let pixel = pixels.data[i];
outputPixels.push( cb(i%4, pixel) );
}
return outputPixels;
}
//callback to apply to each pixel (randomize)
function randomRGB(colour){
if( colour === 3){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB(colour, value){
if(colour === 3){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
let t0 = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
let t1 = performance.now();
console.log(t1-t0+"ms");
});
#canvas {
position: absolute;
top: 0;
left: 0;
}
<canvas id="canvas"></canvas>
code gist: https://gist.github.com/GCDeveloper/c02ffff1d067d6f1b1b13341a72efe79
check out https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas which should help, including loading an actual image as ImageData for usage.
Let's say I have an array of objects.
I have 3 values associated with this array: min-height, max-height and average-height.
I want to assign a height to each object so that:
No object's height is less than min-height
No object's height is greater than max-height
The mean of all objects' heights is average-height.
Essentially I am looking to generate a height distribution like this:
The heights have to be pseudo-random - that is to say, I want to be able to get a height for each object by feeding the result of a random number generator into a function and getting the height returned.
My solution at the moment is to split my range of acceptable heights (all between min-height and max-height) into a series of bins and assign a probability to each bin. Once a bin is selected, I choose a height from within that range at random.
This is not an ideal solution as it is inelegant, clunky, and produces a stepped curve as opposed to a smooth one.
Here is my current code for producing the bins:
var min_height = 10
var max_height = 100
var avg_height = 30
var scale = SCALE ()
.map_from([min_height, avg_height, max_height])
.map_to([-Math.PI, 0, Math.PI])
var range = max_height - min_height;
var num_of_bins = 10
var bin_size = range/num_of_bins;
var bins = []
var sum_of_probability = 0
while (bins.length < num_of_bins) {
var bin = {};
bin.min = min_height + (bins.length*bin_size);
bin.max = bin.min + bin_size;
bin.mid = bin.min + (bin_size/2);
bin.probability = Math.cos(scale(bin.mid))+1
sum_of_probability += bin.probability;
bins.push(bin)
}
var i;
var l = bins.length;
for (i=0; i<l; i++) {
bins[i].probability /= sum_of_probability
if (bins[i-1]) {
bins[i].cumulative_probability = bins[i-1].cumulative_probability + bins[i].probability;
}
else {
bins[i].cumulative_probability = bins[i].probability;
}
}
Essentially I would love to be able to generate pseudo-random data to roughly fit a curve in an elegant way, and I am not sure if this is possible in javascript. Let me know if you think this is do-able.
I borrowed the Gaussian "class" from here: html5 draw gaussian function using bezierCurveTo.
The stuff that's really relevant to you is the getPoints() function. Basically, given a min, max and average height, getPoints() will return an array with a smooth gaussian curve of values. You can then take those points and scale them over whatever range you would need (just multiply them).
The numSteps value of generateValues (which getPoints has hard-coded to 1000) controls how many values you get back, giving you a better "resolution". If you did something like 10, you'd have the values for something like your bar graph. Given 1000 gives a nice smooth curve.
Hope this helps.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 200;
var Gaussian = function(mean, std) {
this.mean = mean;
this.std = std;
this.a = 1/Math.sqrt(2*Math.PI);
};
Gaussian.prototype = {
addStd: function(v) {
this.std += v;
},
get: function(x) {
var f = this.a / this.std;
var p = -1/2;
var c = (x-this.mean)/this.std;
c *= c;
p *= c;
return f * Math.pow(Math.E, p);
},
generateValues: function(start, end, numSteps = 100) {
var LUT = [];
var step = (Math.abs(start)+Math.abs(end)) / numSteps;
for(var i=start; i<end; i+=step) {
LUT.push(this.get(i));
}
return LUT;
}
};
const getPoints = () => {
const minHeight = 0;
const maxHeight = 200;
const averageHeight = 50;
const start = -10;
const end = 10;
const mean = averageHeight / (maxHeight - minHeight) * (end - start) + start;
const std = 1;
const g = new Gaussian(mean, std);
return g.generateValues(start, end, 1000);
}
const draw = () => {
const points = getPoints();
// x-axis
ctx.moveTo(0, canvas.height - 20);
ctx.lineTo(canvas.width, canvas.height - 20);
// y-axis
ctx.moveTo(canvas.width / 2, 0);
ctx.lineTo(canvas.width / 2, canvas.height);
ctx.moveTo(0, canvas.height - 20);
console.log(points);
for (let i = 0; i < points.length; i++) {
ctx.lineTo(i * (canvas.width / points.length),
canvas.height - points[i] * canvas.height - 20);
}
ctx.stroke();
};
draw();
body {
background: #000;
}
canvas {
background: #FFF;
}
<canvas></canvas>
I have a problem on my project.
I am developing a perspective mockup creating module for designers. Users upload images and i get them for placing in mockups with making some perspective calculations. Then users can download this image. I made all of this on clientside with js.
But there is a problem for images which are drawn on canvas with perspective calculations like this;
Sample img: http://oi62.tinypic.com/2h49dec.jpg
orginal image size: 6500 x 3592 and you can see spread edges on image...
I tried a few technics like ctx.imageSmoothingEnabled true etc.. But result was always same.
What can i do for solve this problem? What do you think about this?
edit
For more detail;
I get an image (Resolution free) from user then crop it for mockup ratio. For example in my sample image, user image was cropped for imac ratio 16:9 then making calculation with four dot of screen. By the way, my mockup image size is 6500 x 3592. so i made scale, transform etc this cropped image and put it in mockup on canvas. And then use blob to download this image to client...
Thanks.
Solved.
I use perspective.js for calculation on canvas. so I made some revisions on this js source.
If you wanna use or check source;
// Copyright 2010 futomi http://www.html5.jp/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// perspective.js v0.0.2
// 2010-08-28
/* -------------------------------------------------------------------
* define objects (name space) for this library.
* ----------------------------------------------------------------- */
if (typeof html5jp == 'undefined') {
html5jp = new Object();
}
(function() {
html5jp.perspective = function(ctxd, image) {
// check the arguments
if (!ctxd || !ctxd.strokeStyle) {
return;
}
if (!image || !image.width || !image.height) {
return;
}
// prepare a <canvas> for the image
var cvso = document.createElement('canvas');
cvso.width = parseInt(image.width) * 2;
cvso.height = parseInt(image.height) * 2;
var ctxo = cvso.getContext('2d');
ctxo.drawImage(image, 0, 0, cvso.width, cvso.height);
// prepare a <canvas> for the transformed image
var cvst = document.createElement('canvas');
cvst.width = ctxd.canvas.width;
cvst.height = ctxd.canvas.height;
var ctxt = cvst.getContext('2d');
ctxt.imageSmoothingEnabled = true;
ctxt.mozImageSmoothingEnabled = true;
ctxt.webkitImageSmoothingEnabled = true;
ctxt.msImageSmoothingEnabled = true;
// parameters
this.p = {
ctxd: ctxd,
cvso: cvso,
ctxo: ctxo,
ctxt: ctxt
}
};
var proto = html5jp.perspective.prototype;
proto.draw = function(points) {
var d0x = points[0][0];
var d0y = points[0][1];
var d1x = points[1][0];
var d1y = points[1][1];
var d2x = points[2][0];
var d2y = points[2][1];
var d3x = points[3][0];
var d3y = points[3][1];
// compute the dimension of each side
var dims = [
Math.sqrt(Math.pow(d0x - d1x, 2) + Math.pow(d0y - d1y, 2)), // top side
Math.sqrt(Math.pow(d1x - d2x, 2) + Math.pow(d1y - d2y, 2)), // right side
Math.sqrt(Math.pow(d2x - d3x, 2) + Math.pow(d2y - d3y, 2)), // bottom side
Math.sqrt(Math.pow(d3x - d0x, 2) + Math.pow(d3y - d0y, 2)) // left side
];
//
var ow = this.p.cvso.width;
var oh = this.p.cvso.height;
// specify the index of which dimension is longest
var base_index = 0;
var max_scale_rate = 0;
var zero_num = 0;
for (var i = 0; i < 4; i++) {
var rate = 0;
if (i % 2) {
rate = dims[i] / ow;
} else {
rate = dims[i] / oh;
}
if (rate > max_scale_rate) {
base_index = i;
max_scale_rate = rate;
}
if (dims[i] == 0) {
zero_num++;
}
}
if (zero_num > 1) {
return;
}
//
var step = 0.10;
var cover_step = step * 250;
//
var ctxo = this.p.ctxo;
var ctxt = this.p.ctxt;
//*** ctxt.clearRect(0, 0, ctxt.canvas.width, ctxt.canvas.height);
if (base_index % 2 == 0) { // top or bottom side
var ctxl = this.create_canvas_context(ow, cover_step);
var cvsl = ctxl.canvas;
for (var y = 0; y < oh; y += step) {
var r = y / oh;
var sx = d0x + (d3x - d0x) * r;
var sy = d0y + (d3y - d0y) * r;
var ex = d1x + (d2x - d1x) * r;
var ey = d1y + (d2y - d1y) * r;
var ag = Math.atan((ey - sy) / (ex - sx));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / ow;
ctxl.setTransform(1, 0, 0, 1, 0, -y);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
} else if (base_index % 2 == 1) { // right or left side
var ctxl = this.create_canvas_context(cover_step, oh);
var cvsl = ctxl.canvas;
for (var x = 0; x < ow; x += step) {
var r = x / ow;
var sx = d0x + (d1x - d0x) * r;
var sy = d0y + (d1y - d0y) * r;
var ex = d3x + (d2x - d3x) * r;
var ey = d3y + (d2y - d3y) * r;
var ag = Math.atan((sx - ex) / (ey - sy));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / oh;
ctxl.setTransform(1, 0, 0, 1, -x, 0);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
}
// set a clipping path and draw the transformed image on the destination canvas.
this.p.ctxd.save();
this.set_clipping_path(this.p.ctxd, [
[d0x, d0y],
[d1x, d1y],
[d2x, d2y],
[d3x, d3y]
]);
this.p.ctxd.drawImage(ctxt.canvas, 0, 0);
this.p.ctxd.restore();
}
proto.create_canvas_context = function(w, h) {
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.mozImageSmoothingEnabled = true;
ctx.webkitImageSmoothingEnabled = true;
ctx.msImageSmoothingEnabled = true;
return ctx;
};
proto.set_clipping_path = function(ctx, points) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.closePath();
ctx.clip();
};
})();
The problem is (most likely, but no code shows so..) that the image is actually too big.
The canvas typically uses bi-linear interpolation (2x2 samples) rather than bi-cubic (4x4 samples). That means if you scale it down a large percentage in one chunk the algorithm will skip some pixels that otherwise should have been sampled, resulting in a more pixelated look.
The solution do is to resize the image in steps, ie. 50% of itself repeatably until a suitable size is achieved. Then use perspective calculations on it. The exact destination size is something you need to find by trial and error, but a good starting point is to use the largest side of the resulting perspective image.
Here is one way to step-down rescale an image in steps.