How to measure text with whitespace: pre on canvas? - html

You can measure text to get the width of a sentence like this:
ctx.measureText('I am a sentence').width
You can do this because you set the font on the context directly to match whatever CSS-styled text in your HTML that you're trying to really measure.
ctx.font = '24px MyFont';
But what if I want to apply whitespace: pre to the text? I seem to be getting weird results that don't measure up.
What I'm trying to do is simply calculate the words that can fit safely on a single line. That's it. But sometimes what I measure is smaller than what gets rendered, so I am starting to suspect that the canvas measurement technique isn't actually an accurate tool.
How do I accurately measure the width of a sentence taking into account all the features of the styling such as letter-spacing, whitespace: pre, etc.?

Use CSS and the Range API, which offers a getBoundingClientRect method, this is still the most powerful combination we have access to while waiting for the Font metrics API.
const target = document.getElementById( 'target' );
const range = document.createRange();
const bounds = document.getElementById( 'bounds' );
range.selectNode( target.firstChild );
onresize = e => {
const rect = range.getBoundingClientRect();
bounds.style.left = rect.x + 'px';
bounds.style.top = rect.y + 'px';
bounds.style.width = rect.width + 'px';
bounds.style.height = rect.height + 'px';
};
onresize();
#bounds {
position: absolute;
border: 1px solid;
}
<pre id="target">
Draw some rect
around
me
</pre>
<div id="bounds"></div>
You can see a more complete example of this combination in order to draw on a canvas, probably more like you need, on this related Q/A.

Related

AS3 custom TextField text is being drawn outside its textWidth

So this one is a little hard to explain. I have a custom Text class that automatically resizes and sets the width of the text when you change its value. I then take that Text and draw it on a Bitmap to scale it up to make the text look pixelated.
I have a property called maxWidth that allows you to restrict the width of the text if you want it to maintain a certain width. By default the maxWidth is the width of the text's parent so that it doesn't get cut off or expand the parent's boundaries unexpectedly.
So unfortunately when I draw the text it sometimes gets cut off on the right side. Now I've checked all the values and the width and textWidth are showing up as within their maxWidth values, but when I take a look myself through screenshots I see the text is actually about 3 pixels wider than it should be.
Here's an image to better explain what I mean:
I turned on borders so you can easily see what I mean. The word "and" on the first line gets drawn outside its border. Here is the line of code that handles resizing text when you change its bounds.
override protected function checkResize(value:String):void {
var bufferWidth:uint = Math.floor(Number(defaultTextFormat.size) / bufferDivisor) + bufferMin;
var maxWidth:Number = this.maxWidth;
x = y = 0;
if (parent is Stage) {
var stageParent:Stage = Stage(parent);
super.width = stageParent.stageWidth;
super.height = stageParent.stageHeight;
if (maxWidth == 0) maxWidth = stageParent.stageWidth;
}
else {
super.width = parent.width;
super.height = parent.height;
if (maxWidth == 0) maxWidth = parent.width;
}
maxWidth = maxWidth / scale;
text = value;
if (textWidth + bufferWidth <= maxWidth) super.width = textWidth + bufferWidth;
else super.width = maxWidth;
super.height = textHeight + 4;
if (textSnapshot) updateSnapshot();
if (alignRelation) Align.alignTo(textSprite, alignRelation, alignDirection, alignXOffset, alignYOffest);
}
And for this text specifically the width value states it's 512, which is correct since that's the maxWidth. However if you notice the top line in the text, it goes beyond the 512 width border, it actually goes all the way to 515 even though it says its width is 512. Even more bizarre is the textWidth states it's 510.4 even though the first line goes well beyond that amount. I just want to know if I'm doing anything wrong or if there's a way to get a true textWidth value.
This seems to be related to embedding fonts, at least it was when I had the same problem. A workaround is to set the right margin of the text field, like so
var tf:TextFormat = new TextFormat();
tf.rightMargin = 10; // or whatever fixes your problem, e.g. relate it to font size
textField.setTextFormat(tf);

HTML draw arrows behind text

Can html5 canvas do the following? If yes, how...
Be places behind bottom layer place behind HTML text
Can you accurately find the coordinates specified HTML texts (perhaps identified with span ID) regardless the of browser zoom size, or line wrap
I am trying to create the following with HTML/CSS/JS:
(please excuse the green squiggly underlines)
The highlighted text could obviously be set with background-color:
The tricky part is connecting the highlighted text with arrows, I would think it might be able to be done with HTLM canvas, but I am open to any ideas.
Also nice little bonus would be the have highlighting/arrows appear on hover or maybe on off button.
PS a little background, the text is some simplified JCL (sort of scripting language for Mainframes) and the highlighted items are files. I am attempting to make it easier to trace the data flow through a job (script). This is pretty simple version but many jobs can be 100s of lines long with lot details that make it hard to trace the which steps related to each other. If there other ideas or tools to help trace the data flow in JCL let me know.
//COBLPGM EXEC PGM=COBLPGM
//INPUT DD DSN=&&SORT,DISP=(OLD,DELETE)
//NACHA DD DSN=NODE.OPER.COBLPGM.OUT(+1)
//SORT2 EXEC PGM=SORT
//SORTIN DD DSN=NODE.OPER.COBLPGM.OUT(+1)
//SORTOUT DD DSN=&&SORT2,DISP=(,PASS)
//SYSIN DD DSN=NODE.OPER.PROCLIB(MEM)
//UNRELATE EXEC PGM=UNPGM
//INPUT DD DSN=NODE.OPER.UNRELATED.FILE
//REPORT DD DSN=&&REPORT
//TSTEMPT1 EXEC PGM=SPOPNCLO
//IN DD DSN=&&SORT2,DISP=(OLD,DELETE)
// IF TSTEMPT1.RC=0 THEN
//SORT3 EXEC PGM=SORT
//SORTIN DD DSN=NODE.OPER.COBLPGM.OUT(+1)
//SORTOUT DD DSN=&&SORT3,DISP=(,PASS),LRECL=141
//SYSIN DD DSN=NODE.OPER.CNTRLCDS(PARM)
// ENDIF
This is just a "conceptual" answer showing that you can track HTML to synchronize a canvas element.
The following code has the text itself in a <pre> tag in HTML. There is a canvas in the background set with fixed size. The canvas is updated on scrolling so the boxes are drawn relative to page (it should also be updated on resize, not shown).
As we can track the text you can see we would also be able to place any other graphics relative to it as well such as arrows and lines. I have not shown this here as I feel it would be too broad, but you should get the gist of it as it shows how to calculate the text line and char positions.
The basis is:
Get absolute position of the <pre> tag
Count number of lines (be careful to place text right after the tag and not on a new line, as well as placing end-tag at the same line as the last text-line)
Dividing absolute height on number of lines will give the line-height in pixels for each line
Use measureText() of context to measure the width of each line by setting context to use the same font and size as the <pre> tag
Use the rectangle from previous pre-tag to offset x and y for the line position.
Each char is calculate using the chars preceding the current, with measureText() (cell being this position and that of the next char).
The text is kept selectable with the canvas marking areas in the background.
Note that special chars in the text-line may throw off measureText (such as && in the example text). These chars must be encoded or replaced before measuring. Replacing is not a problem with a monospaced font such as in this case.
Demo
var pre = document.querySelector("pre"), // get pre ele,ent
rect = pre.getBoundingClientRect(), // get its absolute position
lines = pre.innerHTML.split("\n"), // split text lines
count = lines.length, // count lines
lineH = rect.height / count, // line height
canvas = document.querySelector("canvas"), // setup canvas
ctx = canvas.getContext("2d");
canvas.width = window.innerWidth; // todo: update on resize
canvas.height = window.innerHeight;
ctx.font = "14px monospace"; // use same font in canvas as for pre
ctx.strokeStyle = "#d00";
ctx.translate(0.5, 0.5); // makes lines sharper for demo
window.onscroll = drawBoxes; // we need to track scrolling
drawBoxes();
function drawBoxes() { // render line boxes (y)
ctx.clearRect(0, 0, canvas.width, canvas.height);
for(var i = 0; i < count; i++) {
var w = ctx.measureText(lines[i]).width;
if (w) ctx.strokeRect(rect.left, rect.top + i * lineH - window.scrollY, w, lineH - 1);
showChars(lines[i], rect.top + i * lineH - window.scrollY, lineH);
}
}
function showChars(line, y, h) { // render char lines (x)
ctx.beginPath();
for(var i = 0, ch, x, s = ""; ch = line[i]; i++) {
s += ch;
x = ctx.measureText(s).width;
ctx.moveTo(x, y); ctx.lineTo(x, y + h - 1);
}
ctx.globalAlpha = 0.2;
ctx.stroke();
ctx.globalAlpha = 1;
}
canvas {position:fixed;left:0;top:0;z-index:-1}
pre {font:14px monospace}
<canvas></canvas>
<pre>//COBLPGM EXEC PGM=COBLPGM
//INPUT DD DSN=SORT,DISP=(OLD,DELETE)
//NACHA DD DSN=NODE.OPER.COBLPGM.OUT(+1)
//SORT2 EXEC PGM=SORT
//SORTIN DD DSN=NODE.OPER.COBLPGM.OUT(+1)
//SORTOUT DD DSN=SORT2,DISP=(,PASS)
//SYSIN DD DSN=NODE.OPER.PROCLIB(MEM)
//UNRELATE EXEC PGM=UNPGM
//INPUT DD DSN=NODE.OPER.UNRELATED.FILE
//REPORT DD DSN=REPORT
//TSTEMPT1 EXEC PGM=SPOPNCLO
//IN DD DSN=SORT2,DISP=(OLD,DELETE)
// IF TSTEMPT1.RC=0 THEN
//SORT3 EXEC PGM=SORT
//SORTIN DD DSN=NODE.OPER.COBLPGM.OUT(+1)
//SORTOUT DD DSN=SORT3,DISP=(,PASS),LRECL=141
//SYSIN DD DSN=NODE.OPER.CNTRLCDS(PARM)
// ENDIF</pre>

Pixels twice its size

I just created little pixel art for my project http://i.imgur.com/9FLj6Uk.png. And I want to use it on my site. As you can see, it's small pixel art. I want one pixel to be drawn as 2*2 pixels instead of 1*1 pixel. I would redraw the picture with using 2*2 pixels instead of one but that seems to be bad solution.
I tried to use CSS on it
img.pixel {
width: 32px;
height: 32px
}
but that doesn't work, it shows weird shades in my browser. I want to see hard pixels. Does anyone know any solution for this problem?
This is the problem when I use the CSS above
Pixel art is tough in browsers, mainly due to lack of universal browser support of "pixelated" or "crisp-edges" image rendering. It should be supported in CSS4.
Currently the CSS stack looks like this, although it looks as if Chrome 30 and Opera 16 have broken support for CSS solutions
image-rendering:optimizeSpeed;
image-rendering:-moz-crisp-edges;
image-rendering:-o-crisp-edges;
image-rendering:optimize-contrast;
image-rendering:-webkit-optimize-contrast;
-ms-interpolation-mode: nearest-neighbor;
See this answer by #Phrogz, with a test case. Also see mozilla's documentation on the subject. For universal support now, a JS solution may have to work for the time being such as seen here on the great article drawing pixels is hard:
var resize = function( img, scale ) {
// Takes an image and a scaling factor and returns the scaled image
// The original image is drawn into an offscreen canvas of the same size
// and copied, pixel by pixel into another offscreen canvas with the
// new size.
var widthScaled = img.width * scale;
var heightScaled = img.height * scale;
var orig = document.createElement('canvas');
orig.width = img.width;
orig.height = img.height;
var origCtx = orig.getContext('2d');
origCtx.drawImage(img, 0, 0);
var origPixels = origCtx.getImageData(0, 0, img.width, img.height);
var scaled = document.createElement('canvas');
scaled.width = widthScaled;
scaled.height = heightScaled;
var scaledCtx = scaled.getContext('2d');
var scaledPixels = scaledCtx.getImageData( 0, 0, widthScaled, heightScaled );
for( var y = 0; y < heightScaled; y++ ) {
for( var x = 0; x < widthScaled; x++ ) {
var index = (Math.floor(y / scale) * img.width + Math.floor(x / scale)) * 4;
var indexScaled = (y * widthScaled + x) * 4;
scaledPixels.data[ indexScaled ] = origPixels.data[ index ];
scaledPixels.data[ indexScaled+1 ] = origPixels.data[ index+1 ];
scaledPixels.data[ indexScaled+2 ] = origPixels.data[ index+2 ];
scaledPixels.data[ indexScaled+3 ] = origPixels.data[ index+3 ];
}
}
scaledCtx.putImageData( scaledPixels, 0, 0 );
return scaled;
}
Read the article through, the presence of retina displays and mobile safari may add additional complexity to rendering the correct size pixel art. Although with iOS7's mobile safari this may be rectified.
I suggest you to use an svg image, as that will be scalable and get you what you are looking for.
You can read more about the same from the below link.
https://developer.mozilla.org/en/docs/SVG_In_HTML_Introduction
Hope this helps.
Are you looking for this? <img src="http://i.imgur.com/9FLj6Uk.png width="32" height="32">
check fiddle

HTML5: Inverse text-color on canvas

I want to draw text on a canvas in the inverse color of the background (to make sure the text is readible no matter the background color). I believe in oldskool bitblt-ing, this was an XOR operation.
How to do this?
Update: most of the newer browsers now support the blending mode "difference" which can achieve the same result.
context.globalCompositeOperation = "difference";
Updated demo.
Old answer:
One should think that the XOR mode for composition would do this, but unfortunately canvas' XOR only XORs the alpha bits.
By applying the following code we can however receive a result such as this:
You can make an extension to the canvas like this:
CanvasRenderingContext2D.prototype.fillInversedText =
function(txt, x, y) {
//code - see below
}
Now you can call it on the context as the normal fillText, but with a slight change:
ctx.fillInversedText(txt, x, y);
For this to work we do the following first - measure text. Currently we can only calculate width of text and then assume the height. This may or may not work well as fonts can be very tall and so forth. Luckily this will change in the future, but for now:
var tw = this.measureText(txt).width;
var th = parseInt(ctx.font, '10');
th = (th === 0) ? 10 : th; //assume default if no font and size is set
Next thing we need to do is to setup an off-screen canvas to draw the text we want ot invert:
var co = document.createElement('canvas');
co.width = tw;
co.height = th;
Then draw the actual text. Color does not matter as we are only interested in the alpha channel for this canvas:
var octx = co.getContext('2d');
octx.font = this.font;
octx.textBaseline = 'top';
octx.fillText(txt, 0, 0);
Then we extract the pixel buffers for the area we want to draw the inverted text as well as all the pixels for the off-screen canvas which now contains our text:
var ddata = this.getImageData(x, y, tw, th);
var sdata = octx.getImageData(0, 0, tw, th);
var dd = ddata.data; //cache for increased speed
var ds = sdata.data;
var len = ds.length;
And then we invert each pixel where alpha channel for pixel is greater than 0.
for (var i = 0; i < len; i += 4) {
if (ds[i + 3] > 0) {
dd[i] = 255 - dd[i];
dd[i + 1] = 255 - dd[i + 1];
dd[i + 2] = 255 - dd[i + 2];
}
}
Finally put back the inverted image:
this.putImageData(ddata, x, y);
This may seem as a lot of operations, but it goes pretty fast.
Demo (warning if you are sensitive to flicker)
(the psychedelic background is just to have some variations as fiddle needs external images and most are prevented by CORS when we use pixel manipulation).
I've removed my old answer, as it did not solve the question. As of recently, there are new globalCompositeOperations that do all kinds of great things. I've created an example that shows how to obtain inverted text. In case that link breaks, the method is essentially this:
ctx.globalCompositeOperation = "difference";
ctx.fillStyle = "white";
//draw inverted things here
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

How can I work around the iframe scaling bug in chrome 10?

For reference: http://code.google.com/p/chromium/issues/detail?id=71937
Basically, the issue is that if you scale an iframe, the bounds of the contentWindow of the iframe get the scale modifier applied twice. So if, for example, you scale a 100x100 iframe by 50% (style="-webkit-transform: scale(0.5, 0.5);"), the new iframe bounds will be 50x50 but the contentWindow bounds will be 25x25. The actual content is correctly scaled.
I tried setting the innerHeight/innerWidth of the iframe contentWindow and while I was able to update the property, the contentWindow's visible bounds did not change.
Any input welcome, thanks!
For anyone that stumbles along this bug in the future. I solved it by applying the zoom transformations to the html node of the document inside the iframe rather than the iframe itself. I used the following code:
$(function() {
$("#iframe_name").load(function() {
$("#iframe_name").contents().find('html').css('-moz-transform', 'scale(<?=$scale?>)');
$("#iframe_name").contents().find('html').css('-moz-transform-origin', 'left top');
$("#iframe_name").contents().find('html').css('-webkit-transform', 'scale(<?=$scale?>)');
$("#iframe_name").contents().find('html').css('-webkit-transform-origin', 'left top');
$("#iframe_name").contents().find('html').css('transform', 'scale(<?=$scale?>)');
$("#iframe_name").contents().find('html').css('transform-origin', 'left top');
if (navigator.appVersion.match(/MSIE/)) {
$("#iframe_name").contents().find('html').css('zoom', '<?=(100*$scale)?>%');
}
});
});
This is using php and $scale is something like 0.5 for 50% of the size, or 2 for twice the size. It also uses jQuery.
Here's a solution that uses jQuery (1.4.2). The applied scale can be passed into the function, which then applies the appropriate reverse scale to the iframe. Targets versions of Chrome starting with 10.0.648
function iframeFix (scale) {
// Chrome 10 preview fix
// See: http://code.google.com/p/chromium/issues/detail?id=71937
//
// iframe is scaled twice in this version of Chrome.
// Fix is to apply a reverse scale on the iframe
var chrome_preview_bug_version = "Chrome/10.0.648";
if (navigator.userAgent.indexOf(chrome_preview_bug_version) > -1) {
var scale_fix = Math.sqrt(1/scale);
$('iframe').css({
'-webkit-transform': 'scale(' + scale_fix + ')',
'-webkit-transform-origin': '0 0'
});
}
}
We are currently working around this issue by increasing the size of the iframe such that the inner content window is the original size of the iframe, then repositioning the iframe such that the content window matches the original position of the iframe, then clipping the iframe so that only the content window is visible.
So, in our case, the code is something like:
var chromefix = 7;
var scale = .147;
frm.style.webkitTransform = 'scale(' + scale +',' + scale +')';
frm.style.width = basew/scale*chromefix + "px";
frm.style.height = baseh/scale*chromefix + "px";
frm.style.left = ( basex - ((basew/scale-basew)/2)*chromefix ) + "px";
frm.style.top = ( basey - ((baseh/scale-baseh)/2)*chromefix ) + "px";
frm.style.clip= 'rect(0px,'+basew/scale+'px,'+baseh/scale+'px, 0px)';