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>
Related
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.
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);
I've read a lot of StackOverflow answers and other pages talking about how to do letter spacing in Canvas. One of the more useful ones was Letter spacing in canvas element
As that other question said, 'I've got this canvas element that I'm drawing text to. I want to set the letter spacing similar to the CSS letter-spacing attribute. By that I mean increasing the amount of pixels between letters when a string is drawn.' Note that letter spacing is sometimes, and incorrectly, referred to as kerning.
I notice that the general approach seems to be to output the string on a letter by letter basis, using measureText(letter) to get the letter's width and then adding additional spacing. The problem with this is it doesn't take into account letter kerning pairs and the like. See the above link for an example of this and related comments.
Seems to me that the way to do it, for a line spacing of 'spacing', would be to do something like:
Start at position (X, Y).
Measure wAll, the width of the entire string using measureText()
Remove the first character from the string
Print the first character at position (X, Y) using fillText()
Measure wShorter, the width of the resulting shorter string using measureText().
Subtract the width of the shorter string from the width of the entire string, giving the kerned width of the character, wChar = wAll - wShorter
Increment X by wChar + spacing
wAll = wShorter
Repeat from step 3
Would this not take into account kerning? Am I missing something? Does measureText() add a load of padding that varies depending on the outermost character, or something, and if it does, would not fillText() use the same system to output the character, negating that issue? Someone in the link above mentioned 'pixel-aligned font hinting' but I don't see how that applies here. Can anyone advise either generally or specifically if this will work or if there are problems with it?
EDIT: This is not a duplicate of the other question - which it links to and refers to. The question is NOT about how to do 'letter spacing in canvas', per the proposed duplicate; this is proposing a possible solution (which as far as I know was not suggested by anyone else) to that and other questions, and asking if anyone can see or knows of any issues with that proposed solution - i.e. it's asking about the proposed solution and its points, including details of measureText(), fillText() and 'pixel-aligned font hinting'.
Well, I've written the code, based on the pseudocode above, and done a few comparisons by screenshotting and eyeballing it for differences (zoomed, using straight lines from eg clip boxes to compare X position and width for each character). Looks exactly the same for me, with spacing set at 0.
Here's the HTML:
<canvas id="Test1" width="800px" height="200px"><p>Your browser does not support canvas.</p></canvas>
Here's the code:
this.fillTextWithSpacing = function(context, text, x, y, spacing)
{
//Start at position (X, Y).
//Measure wAll, the width of the entire string using measureText()
wAll = context.measureText(text).width;
do
{
//Remove the first character from the string
char = text.substr(0, 1);
text = text.substr(1);
//Print the first character at position (X, Y) using fillText()
context.fillText(char, x, y);
//Measure wShorter, the width of the resulting shorter string using measureText().
if (text == "")
wShorter = 0;
else
wShorter = context.measureText(text).width;
//Subtract the width of the shorter string from the width of the entire string, giving the kerned width of the character, wChar = wAll - wShorter
wChar = wAll - wShorter;
//Increment X by wChar + spacing
x += wChar + spacing;
//wAll = wShorter
wAll = wShorter;
//Repeat from step 3
} while (text != "");
}
Code for demo/eyeball test:
element1 = document.getElementById("Test1");
textContext1 = element1.getContext('2d');
textContext1.font = "72px Verdana, sans-serif";
textContext1.textAlign = "left";
textContext1.textBaseline = "top";
textContext1.fillStyle = "#000000";
text = "Welcome to go WAVE";
this.fillTextWithSpacing(textContext1, text, 0, 0, 0);
textContext1.fillText(text, 0, 100);
Ideally I'd throw multiple random strings at it and do a pixel by pixel comparison. I'm also not sure how good Verdana's default kerning is, though I understand it's better than Arial - suggestions on other fonts to try gratefully accepted.
So... so far it looks good. In fact it looks perfect.
Still hoping that someone will point out any flaws in the process.
In the meantime I will put this here for others to see if they are looking for a solution on this.
My answer got deleted.
So, I'm using chrome and here is my complete code.
second_image = $('#block_id').first();
canvas = document.getElementById('canvas');
canvas.style.letterSpacing = '2px';
ctx = canvas.getContext('2d');
canvas.crossOrigin = "Anonymous";
canvasDraw = function(text, font_size, font_style, fill_or_stroke){
canvas.width = second_image.width();
canvas.height = second_image.height();
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(second_image.get(0), 0, 0, canvas.width, canvas.height);
//refill text
ctx.font = font_size +'px '+ font_style + ',Symbola';
$test = ctx.font;
ctx.textAlign = "center";
if(fill_or_stroke){
ctx.fillStyle = "#d2b76d";
ctx.strokeStyle = "#9d8a5e";
ctx.strokeText(text,canvas.width*$left,canvas.height*$top);
ctx.fillText(text,canvas.width*$left,canvas.height*$top);
}
else{
ctx.strokeStyle = "#888888";
ctx.strokeText(text,canvas.width*$left,canvas.height*$top);
}
};
And you don't need to use this function this.fillTextWithSpacing. I didn't use and it worked like a charm)
I used canvas.fillText to draw Chinese font in canvas, but the words didn't wrap. I read the canvas tutorial here, but it splits words using space, which won't work for Chinese fonts. Could someone have an experience on this or just show me where to find the solution?
Thank you.
You will need to use the measureText() to measure line width by adding one and one glyph until you get a value exceeding the available space which is where you wrap the text.
For example - I made this loop which will wrap the line when there is no more space available:
ONLINE DEMO HERE
var txt = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
i = 0, ...;
/// loop trough the txt which holds the string
for(; i < txt.length; i++) {
/// measure current width
lw = ctx.measureText(line).width;
/// if within available space add a char to line
if (lw < w - ctx.measureText(txt[i]).width) {
line += txt[i];
} else {
/// didn't fit so draw what we have
ctx.fillText(line, x, y);
/// reset line with the left-over char
line = txt[i];
/// reset x (not used in demo) and increment y position
x = 0;
y += 50;
}
}
/// if anything was left in line draw it here
if (line.length > 0) ctx.fillText(line, x, y);
PS: I didn't have a Chinese font handy for the demo but you'll see the principle.
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