Write Text Anywhere With HTML5 Canvas
Published: May 26, 2011
When working on a project recently I came across the need to be able to add some text to a canvas element. This by itself wasn't particularly hard, as there are several examples of creating text editor apps with canvas, but those all forced the text into a row style grid like a text editor. What I needed was a little more freeform in that I wanted the user to click and the text to show at that point.
After a little hacking around I came up with something that handled my needs and hopefully may help someone else in the future.
(This doesn't look the best in the end, but you can easily add some css styles to make it look better)
Click Here To See Demo
So first we need to create a canvas element
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="main">
<canvas id="c"></canvas><!-- the canvas -->
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
</body>
</html>
Now we have the canvas element all set up we can start having some fun.
Well not quite. First we need to set some dimensions for the canvas element. Without them, it doesn't have any way to know where to place our "drawings".
Add this into a css file
#c {
width: 640px;
height: 360px;
}
Now we can start adding drawings. One thing we are going to need for this is a text entry box. I debated trying to "type" directly into the canvas but decided against it. (for complexity reasons)
The method I chose was to add a textarea to the position of the click in the canvas. The user will enter their text into the textarea popup and hit save. This will then draw the text to the screen.
Let's go ahead and create a javascript function to handle the click and add the textarea and save button.
//steal the mousedown event for the canvas for one time - you can use a regular click if you want - this was to interact with some other code in my project
$('#c').mousedown(function(e){
if ($('#textAreaPopUp').length == 0) {
var mouseX = e.pageX - this.offsetLeft + $("#c").position().left;
var mouseY = e.pageY - this.offsetTop;
//append a text area box to the canvas where the user clicked to enter in a comment
var textArea = "<div id='textAreaPopUp' style='position:absolute;top:"+mouseY+"px;left:"+mouseX+"px;z-index:30;'><textarea id='textareaTest' style='width:100px;height:50px;'></textarea>";
var saveButton = "<input type='button' value='save' id='saveText' onclick='saveTextFromArea("+mouseY+","+mouseX+");'></div>";
var appendString = textArea + saveButton;
$("#main").append(appendString);
} else {
$('textarea#textareaTest').remove();
$('#saveText').remove();
$('#textAreaPopUp').remove();
var mouseX = e.pageX - this.offsetLeft + $("#c").position().left;
var mouseY = e.pageY - this.offsetTop;
//append a text area box to the canvas where the user clicked to enter in a comment
var textArea = "<div id='textAreaPopUp' style='position:absolute;top:"+mouseY+"px;left:"+mouseX+"px;z-index:30;'><textarea id='textareaTest' style='width:100px;height:50px;'></textarea>";
var saveButton = "<input type='button' value='save' id='saveText' onclick='saveTextFromArea("+mouseY+","+mouseX+");'></div>";
var appendString = textArea + saveButton;
$("#main").append(appendString);
}
});
There’s nothing too crazy in here. It does do a check and destroy the textarea if one is already displayed while the user clicks the canvas again.
var mouseX = e.pageX - this.offsetLeft + $("#c").position().left;
var mouseY = e.pageY - this.offsetTop;
These are setting the position of the mouse click so we can use them to set the position to display the textarea at. It takes into account any offsets the canvas element may have as well.
The rest is just appending the textarea to the main element in the page. The save button has an onclick event that we will deal with in a bit.
Now we have to actually draw the entered text to the screen.
function saveTextFromArea(y,x){
//get the value of the textarea then destroy it and the save button
var text = $('textarea#textareaTest').val();
$('textarea#textareaTest').remove();
$('#saveText').remove();
//get the canvas and add the text functions
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var cw = canvas.clientWidth;
var ch = canvas.clientHeight;
canvas.width = cw;
canvas.height = ch;
//break the text into arrays based on a text width of 100px
var phraseArray = getLines(ctx,text,100);
// this adds the text functions to the ctx
CanvasTextFunctions.enable(ctx);
var counter = 0;
//set the font styles
var font = "sans";
var fontsize = 16;
ctx.strokeStyle = "rgba(237,229,0,1)";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 1;
ctx.shadowColor = "rgba(0,0,0,1)";
//draw each phrase to the screen, making the top position 20px more each time so it appears there are line breaks
$.each(phraseArray, function() {
//set the placement in the canvas
var lineheight = fontsize * 1.5;
var newline = ++counter;
newline = newline * lineheight;
var topPlacement = y - $("#c").position().top + newline;
var leftPlacement = x - $("#c").position().left;
text = this;
//draw the text
ctx.drawText(font, fontsize, leftPlacement, topPlacement, text);
ctx.save();
ctx.restore();
});
//reset the drop shadow so any other drawing don't have them
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
ctx.shadowColor = "rgba(0,0,0,0)";
}
So this is fairly intense. It also uses a little library file from Jim Studt that you can find here . Though I have modified mine a bit because of some conflicts it was causing me. You can grab my copy at https://github.com/ninetwentyfour/Video-Canvas/blob/master/js/text.js
A few important parts in that code are
//break the text into arrays based on a text width of 100px
var phraseArray = getLines(ctx,text,100);
This is going to another function that we will write in a minute. Basically it is solving the problem of canvas text not having any idea how to word wrap. It will blow right off the page without this.
// this adds the text functions to the ctx
CanvasTextFunctions.enable(ctx);
This intializes the text library file.
//draw each phrase to the screen, making the top position 20px more each time so it appears there are line breaks
$.each(phraseArray, function() {
//set the placement in the canvas
var lineheight = fontsize * 1.5;
var newline = ++counter;
newline = newline * lineheight;
var topPlacement = y - $("#c").position().top + newline;
var leftPlacement = x - $("#c").position().left;
text = this;
//draw the text
ctx.drawText(font, fontsize, leftPlacement, topPlacement, text);
ctx.save();
ctx.restore();
});
This is the block that actually draws the text to the screen. It takes each entry in the phraseArray and draws it to the screen moving it down by a line each time.
The code to control the width of the text looks like this
function getLines(ctx,phrase,maxPxLength) {
//break the text area text into lines based on "box" width
var wa=phrase.split(" "),
phraseArray=[],
lastPhrase="",
l=maxPxLength,
measure=0;
ctx.font = "16px sans-serif";
for (var i=0;i<wa.length;i++) {
var w=wa[i];
measure=ctx.measureText(lastPhrase+w).width;
if (measure<l) {
lastPhrase+=(" "+w);
}else {
phraseArray.push(lastPhrase);
lastPhrase=w;
}
if (i===wa.length-1) {
phraseArray.push(lastPhrase);
break;
}
}
return phraseArray;
}
This takes the canvas variable the original text and the width you want to limit it to then returns an array of pharses limited to the length you pass.
Once you have all this done you should have a working example. My final code looks like this.
<!DOCTYPE html>
<html>
<head>
<style>
body {
background: #111;
color:#eee;
}
#c {
width: 640px;
height: 360px;
border:1px solid #111;
float:left;
background:#eee;
}
#main{
width:650px;
margin:auto;
}
</style>
</head>
<body>
<div id="main">
<canvas id="c"></canvas><!-- the canvas -->
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
<script type="text/javascript" src="text.js"></script><!-- Library to help text -->
<script type="text/javascript">
$('#c').mousedown(function(e){
if ($('#textAreaPopUp').length == 0) {
var mouseX = e.pageX - this.offsetLeft + $("#c").position().left;
var mouseY = e.pageY - this.offsetTop;
//append a text area box to the canvas where the user clicked to enter in a comment
var textArea = "<div id='textAreaPopUp' style='position:absolute;top:"+mouseY+"px;left:"+mouseX+"px;z-index:30;'><textarea id='textareaTest' style='width:100px;height:50px;'></textarea>";
var saveButton = "<input type='button' value='save' id='saveText' onclick='saveTextFromArea("+mouseY+","+mouseX+");'></div>";
var appendString = textArea + saveButton;
$("#main").append(appendString);
} else {
$('textarea#textareaTest').remove();
$('#saveText').remove();
$('#textAreaPopUp').remove();
var mouseX = e.pageX - this.offsetLeft + $("#c").position().left;
var mouseY = e.pageY - this.offsetTop;
//append a text area box to the canvas where the user clicked to enter in a comment
var textArea = "<div id='textAreaPopUp' style='position:absolute;top:"+mouseY+"px;left:"+mouseX+"px;z-index:30;'><textarea id='textareaTest' style='width:100px;height:50px;'></textarea>";
var saveButton = "<input type='button' value='save' id='saveText' onclick='saveTextFromArea("+mouseY+","+mouseX+");'></div>";
var appendString = textArea + saveButton;
$("#main").append(appendString);
}
});
function saveTextFromArea(y,x){
//get the value of the textarea then destroy it and the save button
var text = $('textarea#textareaTest').val();
$('textarea#textareaTest').remove();
$('#saveText').remove();
$('#textAreaPopUp').remove();
//get the canvas and add the text functions
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var cw = canvas.clientWidth;
var ch = canvas.clientHeight;
canvas.width = cw;
canvas.height = ch;
//break the text into arrays based on a text width of 100px
var phraseArray = getLines(ctx,text,100);
// this adds the text functions to the ctx
CanvasTextFunctions.enable(ctx);
var counter = 0;
//set the font styles
var font = "sans";
var fontsize = 16;
ctx.strokeStyle = "rgba(237,229,0,1)";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 1;
ctx.shadowColor = "rgba(0,0,0,1)";
//draw each phrase to the screen, making the top position 20px more each time so it appears there are line breaks
$.each(phraseArray, function() {
//set the placement in the canvas
var lineheight = fontsize * 1.5;
var newline = ++counter;
newline = newline * lineheight;
var topPlacement = y - $("#c").position().top + newline;
var leftPlacement = x - $("#c").position().left;
text = this;
//draw the text
ctx.drawText(font, fontsize, leftPlacement, topPlacement, text);
ctx.save();
ctx.restore();
});
//reset the drop shadow so any other drawing don't have them
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
ctx.shadowColor = "rgba(0,0,0,0)";
}
function getLines(ctx,phrase,maxPxLength) {
//break the text area text into lines based on "box" width
var wa=phrase.split(" "),
phraseArray=[],
lastPhrase="",
l=maxPxLength,
measure=0;
ctx.font = "16px sans-serif";
for (var i=0;i<wa.length;i++) {
var w=wa[i];
measure=ctx.measureText(lastPhrase+w).width;
if (measure<l) {
lastPhrase+=(" "+w);
}else {
phraseArray.push(lastPhrase);
lastPhrase=w;
}
if (i===wa.length-1) {
phraseArray.push(lastPhrase);
break;
}
}
return phraseArray;
}
</script>
</body>
</html>
This is a pretty basic and is missing a few things that would make it more helpful but it should give you a good start. Please feel free to leave questions or advice on better ways to do this in the comments.