Thorn of text outline in canvas - html

I'm using html5 canvas to draw some texts, but I got some ugly results, here is my sample code to draw the text:
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var x = 80;
var y = 110;
context.fillStyle = "white"
context.font = '13px Arial';
context.lineWidth = 3;
// stroke color
context.strokeStyle = 'black';
context.strokeText('ABCDEFGHIJKLMNOPQRSTUVWXYZ!', x, y);
context.fillText('ABCDEFGHIJKLMNOPQRSTUVWXYZ!', x, y);
And I got this result
then I change the text to "abs" and got this
You can see the "M" and "s" looks ugly, does anyone have an idea on how to solve this?

It's not due to the typeface nor size, but due to how the lines in the stroke path are connected.
By changing the line-join method the spikes will go away:
context.lineJoin = 'round';

Also, if you prefer sharp corners (without the miter-spikes,) try this:
context.lineJoin = 'miter';
context.miterLimit = 2;
http://jsfiddle.net/hwG42/3/
It does the trick.

You might get better results when you use a different font.
Alternatively, you could use only fillText and create the outline with a shadow.

Related

HTML5 Canvas Sweep Gradient

It looks like the HTML5 canvas does not support a "sweep gradient" - a gradient where the color stops rotate around the center, rather than emanating from the center.
Is there any way to simulate a sweep gradient on a canvas? I suppose I could do something similar with lots of little linear gradients, but at that point I'm basically rendering the gradient myself.
Indeed there is no built-in for such a thing.
Not sure what you had in mind with these "lots of little linear gradients", but you actually just need a single one, the size of your circle's circumference, and only to get the correct colors to use.
What you'll need a lot though are lines, since we'll draw these around the center point using the solid colors we had in the linearGradient.
So to render this, you just move to the center point, then draw a line using a solid color from the linear gradient, then rotate and repeat.
To get all the colors of a linearGradient, you just need to draw it and map it's ImageData to CSS colors.
The hard part though is that to be able to have an object that behaves like a CanvasGradient, we need to be able to set it as a fillStyle or strokeStyle.
This is possible by returning a CanvasPattern. An other difficulty is that gradients are virtually infinitely big. A non-repeating Pattern is not.
I didn't found a good solution to overcome this problem, but as a workaround, we can use the size of the target canvas as a limit.
Here is a rough implementation:
class SweepGrad {
constructor(ctx, x, y) {
this.x = x;
this.y = y;
this.target = ctx;
this.colorStops = [];
}
addColorStop(offset, color) {
this.colorStops.push({offset, color});
}
render() {
// get the current size of the target context
const w = this.target.canvas.width;
const h = this.target.canvas.width;
const x = this.x;
const y = this.y;
// get the max length our lines can be
const maxDist = Math.ceil(Math.max(
Math.hypot(x, y),
Math.hypot(x - w, y),
Math.hypot(x - w, y - h),
Math.hypot(x, y - h)
));
// the circumference of our maxDist circle
// this will determine the number of lines we will draw
// (we double it to avoid some antialiasing artifacts at the edges)
const circ = maxDist*Math.PI*2 *2;
// create a copy of the target canvas
const canvas = this.target.canvas.cloneNode();
const ctx = canvas.getContext('2d');
// generate the linear gradient used to get all our colors
const linearGrad = ctx.createLinearGradient(0, 0, circ, 0);
this.colorStops.forEach(stop =>
linearGrad.addColorStop(stop.offset, stop.color)
);
const colors = getLinearGradientColors(linearGrad, circ);
// draw our gradient
ctx.setTransform(1,0,0,1,x,y);
for(let i = 0; i<colors.length; i++) {
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(maxDist, 0);
ctx.strokeStyle = colors[i];
ctx.stroke();
ctx.rotate((Math.PI*2)/colors.length);
}
// return a Pattern so we can use it as fillStyle or strokeStyle
return ctx.createPattern(canvas, 'no-repeat');
}
}
// returns an array of CSS colors from a linear gradient
function getLinearGradientColors(grad, length) {
const canvas = Object.assign(document.createElement('canvas'), {width: length, height: 10});
const ctx = canvas.getContext('2d');
ctx.fillStyle = grad;
ctx.fillRect(0,0,length, 10);
return ctx.getImageData(0,0,length,1).data
.reduce((out, channel, i) => {
const px_index = Math.floor(i/4);
const px_slot = out[px_index] || (out[px_index] = []);
px_slot.push(channel);
if(px_slot.length === 4) {
px_slot[3] /= 255;
out[px_index] = `rgba(${px_slot.join()})`;
}
return out;
}, []);
}
// How to use
const ctx = canvas.getContext('2d');
const redblue = new SweepGrad(ctx, 70, 70);
redblue.addColorStop(0, 'red');
redblue.addColorStop(1, 'blue');
// remeber to call 'render()' to get the Pattern back
// maybe a Proxy could handle that for us?
ctx.fillStyle = redblue.render();
ctx.beginPath();
ctx.arc(70,70,50,Math.PI*2,0);
ctx.fill();
const yellowgreenred = new SweepGrad(ctx, 290, 80);
yellowgreenred.addColorStop(0, 'yellow');
yellowgreenred.addColorStop(0.5, 'green');
yellowgreenred.addColorStop(1, 'red');
ctx.fillStyle = yellowgreenred.render();
ctx.fillRect(220,10,140,140);
// just like with gradients,
// we need to translate the context so it follows our drawing
ctx.setTransform(1,0,0,1,-220,-10);
ctx.lineWidth = 10;
ctx.strokeStyle = ctx.fillStyle;
ctx.stroke(); // stroke the circle
canvas{border:1px solid}
<canvas id="canvas" width="380" height="160"></canvas>
But beware, all this is quite computationally heavy, so be sure to use it sporadically and to cache your resulting Gradients/Patterns.

How can I fill in the outside of a path?

I am able to draw these letters using a path. But what I want to do is use that path and fill in what the red image shows instead of filling in the letters.
Here is the code I am using:
function mattes_draw_letter(x, y, width, height, letter, position)
{
var canvas = document.createElement('canvas');
canvas.style.position = "absolute";
canvas.style.top = y + "px";
canvas.id = "canvas_opening_" + position;
canvas.style.zIndex = 5;
canvas.width = width;
canvas.height = height;
canvas.style.left = x + "px";
var ctx = canvas.getContext("2d");
ctx.lineWidth = 1;
ctx.fillStyle = '#bfbfbf';
ctx.strokeStyle = '#000000';
ctx.beginPath();
ctx.moveTo(letter[0] * width, letter[1] * height);
for (i = 0; i < letter.length; i+=2)
{
if (typeof letter[i+3] !== 'undefined')
{
ctx.lineTo(letter[i+2] * width, letter[i+3] * height);
}
}
ctx.fill();
ctx.stroke();
ctx.closePath();
$("#mattes").append(canvas);
canvas.addEventListener("drop", function(event) {drop(event, this);}, false);
canvas.addEventListener("dragover", function(event) {allowDrop(event);}, false);
canvas.addEventListener("click", function() {photos_add_selected_fid(this);}, false);
}
This is what I currently have:
This is what I would like:
Just fill the boxes with red color before drawing the letters in gray.
I was able to do this by adding two lines of code in your code.
ctx.fillStyle = "#F00";
ctx.fillRect(0, 0, width, height);
Put these two lines between the lines:
ctx.lineWidth = 1;
and
ctx.fillStyle = '#bfbfbf';
I assume you're starting the existing letters otherwise (as #Chirag64 says), you can just draw the red rectangles first and then draw the letters on top).
You can use canvas compositing to "draw behind" existing content.
A Demo: http://jsfiddle.net/m1erickson/695dY/
In particular the destination-over compositing mode will draw new content behind existing content (new content is only drawn where the existing content is transparent).
context.globalCompositeOperation="destination-over";
Assuming the HOPE characters are drawn over a transparent background you can add red rectangles behind the HOPE characters like this:
// draw red rectangles **behind** the letters using compositing
ctx.fillStyle="red";
ctx.globalCompositeOperation="destination-over";
for(var i=0;i<4;i++){
ctx.fillRect(i*62+16,13,50,88); // your x,y,width,height depend on your artwork
}

HTML5 Canvas: Why does measuring text with measureText and offsetWidth() give different values?

I benchmarked offsetWidth() vs measureText and I am getting drastically different values. Shouldn't they be the same? Why are they different?
Here is the jsfiddle and raw code below:
http://jsfiddle.net/WhGk7/2/
<canvas id="myCanvas" width="300" height="200" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
<span id="visibilityHack" style="visibility: hidden; font: 15px Arial;">textAlign=start</span>
<div id="results"></div>
<script>
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
// Create a red line in position 150
ctx.strokeStyle="red";
ctx.moveTo(150,20);
ctx.lineTo(150,170);
ctx.stroke();
var measureTextWidth = ctx.measureText("textAlign=start").width;
var measureTextNode = document.createTextNode("measureTextWidth: " + measureTextWidth);
document.getElementById("results").appendChild(measureTextNode);
var swidth = document.getElementById("visibilityHack").offsetWidth;
var textnode = document.createTextNode(" offsetWidth: " + swidth);
document.getElementById("results").appendChild(textnode);
ctx.font="15px Arial";
// Show the different textAlign values
ctx.textAlign="start";
ctx.fillText("textAlign=start",117,60);
ctx.textAlign="center";
ctx.fillText("textAlign=start",150,120);
</script>
The support for context.measureText is very bad in most browsers. But there is a hack which allows you to get a much better measurement of text. Create a <div> node in your HTML document with visibility: hidden (so it isn't rendered) but not display: none (so it takes up space). Then set its style to the same style you want to use for context.fillText (remember that when you use an external font, that font must be fully loaded to get an accurate measurement), put your text into the div, and check the div's .width
You need to set the font on the canvas context before you do measureText, otherwise you will get whatever the default font style is on the context. You already set the font family and size on the hack div and that is why it is giving you the correct value.
What I did observe though is that Chrome 34 and Firefox 28 both returned 92 for the width, but IE10 returned 95, Grrr.
Canvas support was less accurate in the past.
As of November 2014, most browsers seem to work just fine. Tested Chrome, IE and Firefox. Also note that most browsers' Canvas.measureText functions even yield results with sub-pixel accuracy. See this fiddle for reference.
To save you the trouble of writing your own, you might want to use an existing string-measuring function.
It seems that both measureText and "DOM element method" still do not return real text width.
But context2d.measureText and "OM element method" return very similar values :)
Let's try to measure width of text consisting of single character 'y' and printed with 'italic 90px arial'. You can try it on JSFiddle - i modified the Domi's code http://jsfiddle.net/White_Falkon/a23z6ryL/2/
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* #param text The text to be rendered.
* #param {String} font The css font descriptor that text is to be rendered with (e.g. "14px verdana").
*
* #see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/
function getTextWidth(text, font) {
// if given, use cached canvas for better performance
// else, create new canvas
var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.getElementById("myCanvas"));
var context = canvas.getContext("2d");
var oldFont = context.font;
context.font = font;
var metrics = context.measureText(text);
context.font = oldFont;
return metrics.width;
};
function getTextWidthDOM(text, font) {
var f = font || '12px arial',
o = $('<span>' + text + '</span>')
.css({'font': f, 'float': 'left', 'white-space': 'nowrap'})
//.css({'visibility': 'hidden'})
.appendTo($('body')),
w = o.width();
o.remove();
return w;
}
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
context.clearRect ( 0 , 0 , canvas.width, canvas.height );
var x = canvas.width / 2;
var y = canvas.height / 2 - 100;
var text = 'y';
var font = 'italic 90px Arial';
context.font = font;
context.fillStyle = 'blue';
context.fillText(text, x, y);
// get text metrics
var widthUsingDOM = getTextWidthDOM(text, font);
var widthUsingMeasureText = getTextWidth(text, font);
context.font = '20pt Calibri';
context.textAlign = 'center';
context.fillStyle = 'red';
context.fillText('(' + widthUsingDOM + 'px wide using DOM)', x, y + 100);
context.fillStyle = 'green';
context.fillText('(' + widthUsingMeasureText + 'px wide using measureText)', x, y + 150);
context.beginPath();
context.rect(x, y-75, widthUsingDOM, 125);
context.lineWidth = 1;
context.strokeStyle = 'red';
context.stroke();
context.beginPath();
context.rect(x, y-75, widthUsingMeasureText, 125);
context.lineWidth = 1;
context.strokeStyle = 'green';
context.stroke();
You'll see, that part of 'y' on the right is outside the 'width rectangle'.
Another case, when these measuring methods are incorrect is 'y' printed with 'italic 90px times new roman' - the left part of y is outside of width rectangle. You can try it on the same JSFiddle.
Unfortunately, i don't know if there is a way to measure full width of string.

Can I do by character text color in HTML5 Canvas?

In HTML Canvas I can set the color of a line of text using ctx.fillStyle = 'red', which is great. What I'm trying to do is by able to set the color by letter, with only having to draw the word once.
So if the text is 'Hello different colors!', is there a way I can make the letter H red, but the rest of the text white?
I present you this workaround. Basically you output your text one char at a time, and use inbuilt measureText() function to determine the width of each letter asif it was drawn. Then we offset the position where we wanted to draw by that same amount. You can modify this snippet, to produce the effect you want.
Suppose we have HTML like so:
<canvas id="canvas" width="300" height="300"/>
And Javascript like so:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
function randomColor(){
var r = Math.floor(Math.random()*256);
var g = Math.floor(Math.random()*256);
var b = Math.floor(Math.random()*256);
return "rgb("+ r + "," + g + "," + b +")";
}
function texter(str, x, y){
for(var i = 0; i <= str.length; ++i){
var ch = str.charAt(i);
ctx.fillStyle = randomColor();
ctx.fillText(ch, x, y);
x += ctx.measureText(ch).width;
}
}
texter("What's up?", 10, 30);
We'd get an output:
Check it out in action at jFiddle. I used latest Chrome.
ctx.fillStyle works as a state machine. When you say ctx.fillStyle = 'red', it will color things as red. You can do what you want by setting ctx.fillStyle = 'white', then writing the letter H, then setting ctx.fillStyle = 'red', then writing the rest of the sentence.
If you don't want to use a "round about way" function.
You can use gradients e.g. createLinearGradient. I figured out how to hard block the colors so there was no gradient just two blocks of color. This is what I did:
var gradient = ctx.createLinearGradient(0, 0, 300, 0);
gradient.addColorStop(0, "red");
gradient.addColorStop(0.5, "red");
gradient.addColorStop(0.5, "blue");
gradient.addColorStop(1, "blue");
ctx.fillStyle = gradient;
This code allows text to look like this and also allows for multiple colors of text on one line of text. Check out this example: w3school example. Hope this helps.

Smooth user drawn lines in canvas

I'm using <canvas> to capture user input in the form of a signature and am trying to figure out how to smooth the input from the mouse.
I think I need to process the user's mouse movements chunk by chunk and smooth each chunk, I'm not after super smoothing but any improvement on the jagged input would be good.
Thanks,
Mark
What you want is:
ctx.lineCap = 'round';
Here is an example of how it could be used:
Give it a try http://jsbin.com/ateho3
markup :
<canvas id="canvas"></canvas>
JavaScript :
window.onload = function() {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var width = window.innerWidth;
var height = window.innerHeight;
canvas.height = height;
canvas.width = width;
canvas.addEventListener('mousedown', function(e) {
this.down = true;
this.X = e.pageX ;
this.Y = e.pageY ;
this.color = rgb();
}, 0);
canvas.addEventListener('mouseup', function() {
this.down = false;
}, 0);
canvas.addEventListener('mousemove', function(e) {
this.style.cursor = 'pointer';
if(this.down) {
ctx.beginPath();
ctx.moveTo(this.X, this.Y);
ctx.lineCap = 'round';
ctx.lineWidth = 3;
ctx.lineTo(e.pageX , e.pageY );
ctx.strokeStyle = this.color;
ctx.stroke();
this.X = e.pageX ;
this.Y = e.pageY ;
}
}, 0);
function rgb() {
color = 'rgb(';
for(var i = 0; i< 3; i++) {
color += Math.floor(Math.random() * 255)+',';
}
return color.replace(/\,$/,')');
}
};
I know that this is a 10 years old question but I think the answer is not complete. For a smooth line effect you need to set two properties to the canvas' context :
context.lineCap = 'round'
context.lineJoin = 'round'
The first one is for extremities of one path the second one is for corners of a path.
Some docs on lineJoin.
Some docs on lineCap.
I had to make a smooth canvas drawing for an mobile web application and learned couple of things.
The Answer of Avinash is great but if you increase the line width, when you draw you will see broken lines. It is because the line cap is rectangular by default.
To make the line smoother you need to tweak something a bit.
ctx.lineCap = 'round';
this little tweak will give you a super smooth line.
To know more about this, try the following link
https://developer.mozilla.org/samples/canvas-tutorial/4_6_canvas_linecap.html
How about using Bezier curves?
I haven't tested this in any way, but you could try drawing small circles with a radial fill gradient.
Consider connecting dots by using lines automatically, or even, use quadraticCurveTo, but you must calculate the middle point by yourself.