I am trying to add text on an image using the <canvas> element. First the image is drawn and on the image the text is drawn. So far so good.
But where I am facing a problem is that if the text is too long, it gets cut off in the start and end by the canvas. I don't plan to resize the canvas, but I was wondering how to wrap the long text into multiple lines so that all of it gets displayed. Can anyone point me at the right direction?
Updated version of #mizar's answer, with one severe and one minor bug fixed.
function getLines(ctx, text, maxWidth) {
var words = text.split(" ");
var lines = [];
var currentLine = words[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;
}
}
lines.push(currentLine);
return lines;
}
We've been using this code for some time, but today we were trying to figure out why some text wasn't drawing, and we found a bug!
It turns out that if you give a single word (without any spaces) to the getLines() function, it will return an empty array, rather than an array with a single line.
While we were investigating that, we found another (much more subtle) bug, where lines can end up slightly longer than they should be, since the original code didn't account for spaces when measuring the length of a line.
Our updated version, which works for everything we've thrown at it, is above. Let me know if you find any bugs!
A possible method (not completely tested, but as for now it worked perfectly)
/**
* Divide an entire phrase in an array of phrases, all with the max pixel length given.
* The words are initially separated by the space char.
* #param phrase
* #param length
* #return
*/
function getLines(ctx,phrase,maxPxLength,textStyle) {
var wa=phrase.split(" "),
phraseArray=[],
lastPhrase=wa[0],
measure=0,
splitChar=" ";
if (wa.length <= 1) {
return wa
}
ctx.font = textStyle;
for (var i=1;i<wa.length;i++) {
var w=wa[i];
measure=ctx.measureText(lastPhrase+splitChar+w).width;
if (measure<maxPxLength) {
lastPhrase+=(splitChar+w);
} else {
phraseArray.push(lastPhrase);
lastPhrase=w;
}
if (i===wa.length-1) {
phraseArray.push(lastPhrase);
break;
}
}
return phraseArray;
}
Here was my spin on it... I read #mizar's answer and made some alterations to it... and with a little assistance I Was able to get this.
code removed, see fiddle.
Here is example usage. http://jsfiddle.net/9PvMU/1/ - this script can also be seen here and ended up being what I used in the end... this function assumes ctx is available in the parent scope... if not you can always pass it in.
edit
the post was old and had my version of the function that I was still tinkering with. This version seems to have met my needs thus far and I hope it can help anyone else.
edit
It was brought to my attention there was a small bug in this code. It took me some time to get around to fixing it but here it is updated. I have tested it myself and it seems to work as expected now.
function fragmentText(text, maxWidth) {
var words = text.split(' '),
lines = [],
line = "";
if (ctx.measureText(text).width < maxWidth) {
return [text];
}
while (words.length > 0) {
var split = false;
while (ctx.measureText(words[0]).width >= maxWidth) {
var tmp = words[0];
words[0] = tmp.slice(0, -1);
if (!split) {
split = true;
words.splice(1, 0, tmp.slice(-1));
} else {
words[1] = tmp.slice(-1) + words[1];
}
}
if (ctx.measureText(line + words[0]).width < maxWidth) {
line += words.shift() + " ";
} else {
lines.push(line);
line = "";
}
if (words.length === 0) {
lines.push(line);
}
}
return lines;
}
context.measureText(text).width is what you're looking for...
Try this script to wrap the text on a canvas.
<script>
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
var words = text.split(' ');
var line = '';
for(var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var metrics = ctx.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
ctx.fillText(line, x, y);
line = words[n] + ' ';
y += lineHeight;
}
else {
line = testLine;
}
}
ctx.fillText(line, x, y);
}
var canvas = document.getElementById('Canvas01');
var ctx = canvas.getContext('2d');
var maxWidth = 400;
var lineHeight = 24;
var x = (canvas.width - maxWidth) / 2;
var y = 70;
var text = 'HTML is the language for describing the structure of Web pages. HTML stands for HyperText Markup Language. Web pages consist of markup tags and plain text. HTML is written in the form of HTML elements consisting of tags enclosed in angle brackets (like <html>). HTML tags most commonly come in pairs like <h1> and </h1>, although some tags represent empty elements and so are unpaired, for example <img>..';
ctx.font = '15pt Calibri';
ctx.fillStyle = '#555555';
wrapText(ctx, text, x, y, maxWidth, lineHeight);
</script>
</body>
See demo here http://codetutorial.com/examples-canvas/canvas-examples-text-wrap.
From the script here: http://www.html5canvastutorials.com/tutorials/html5-canvas-wrap-text-tutorial/
I've extended to include paragraph support. Use \n for new line.
function wrapText(context, text, x, y, line_width, line_height)
{
var line = '';
var paragraphs = text.split('\n');
for (var i = 0; i < paragraphs.length; i++)
{
var words = paragraphs[i].split(' ');
for (var n = 0; n < words.length; n++)
{
var testLine = line + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > line_width && n > 0)
{
context.fillText(line, x, y);
line = words[n] + ' ';
y += line_height;
}
else
{
line = testLine;
}
}
context.fillText(line, x, y);
y += line_height;
line = '';
}
}
Text can be formatted like so:
var text =
[
"Paragraph 1.",
"\n\n",
"Paragraph 2."
].join("");
Use:
wrapText(context, text, x, y, line_width, line_height);
in place of
context.fillText(text, x, y);
I am posting my own version used here since answers here weren't sufficient for me. The first word needed to be measured in my case, to be able to deny too long words from small canvas areas. And I needed support for 'break+space, 'space+break' or double-break/paragraph-break combos.
wrapLines: function(ctx, text, maxWidth) {
var lines = [],
words = text.replace(/\n\n/g,' ` ').replace(/(\n\s|\s\n)/g,'\r')
.replace(/\s\s/g,' ').replace('`',' ').replace(/(\r|\n)/g,' '+' ').split(' '),
space = ctx.measureText(' ').width,
width = 0,
line = '',
word = '',
len = words.length,
w = 0,
i;
for (i = 0; i < len; i++) {
word = words[i];
w = word ? ctx.measureText(word).width : 0;
if (w) {
width = width + space + w;
}
if (w > maxWidth) {
return [];
} else if (w && width < maxWidth) {
line += (i ? ' ' : '') + word;
} else {
!i || lines.push(line !== '' ? line.trim() : '');
line = word;
width = w;
}
}
if (len !== i || line !== '') {
lines.push(line);
}
return lines;
}
It supports any variants of lines breaks, or paragraph breaks, removes double spaces, as well as leading or trailing paragraph breaks. It returns either an empty array if the text doesn't fit. Or an array of lines ready to draw.
look at https://developer.mozilla.org/en/Drawing_text_using_a_canvas#measureText%28%29
If you can see the selected text, and see its wider than your canvas, you can remove words, until the text is short enough. With the removed words, you can start at the second line and do the same.
Of course, this will not be very efficient, so you can improve it by not removing one word, but multiple words if you see the text is much wider than the canvas width.
I did not research, but maybe their are even javascript libraries that do this for you
I modified it using the code from here http://miteshmaheta.blogspot.sg/2012/07/html5-wrap-text-in-canvas.html
http://jsfiddle.net/wizztjh/kDy2U/41/
This should bring the lines correctly from the textbox:-
function fragmentText(text, maxWidth) {
var lines = text.split("\n");
var fittingLines = [];
for (var i = 0; i < lines.length; i++) {
if (canvasContext.measureText(lines[i]).width <= maxWidth) {
fittingLines.push(lines[i]);
}
else {
var tmp = lines[i];
while (canvasContext.measureText(tmp).width > maxWidth) {
tmp = tmp.slice(0, tmp.length - 1);
}
if (tmp.length >= 1) {
var regex = new RegExp(".{1," + tmp.length + "}", "g");
var thisLineSplitted = lines[i].match(regex);
for (var j = 0; j < thisLineSplitted.length; j++) {
fittingLines.push(thisLineSplitted[j]);
}
}
}
}
return fittingLines;
And then get draw the fetched lines on the canvas :-
var lines = fragmentText(textBoxText, (rect.w - 10)); //rect.w = canvas width, rect.h = canvas height
for (var showLines = 0; showLines < lines.length; showLines++) { // do not show lines that go beyond the height
if ((showLines * resultFont.height) >= (rect.h - 10)) { // of the canvas
break;
}
}
for (var i = 1; i <= showLines; i++) {
canvasContext.fillText(lines[i-1], rect.clientX +5 , rect.clientY + 10 + (i * (resultFont.height))); // resultfont = get the font height using some sort of calculation
}
This is a typescript version of #JBelfort's answer.
(By the way, thanks for this brilliant code)
As he mentioned in his answer this code can simulate html element such as textarea,and also the CSS property
word-break: break-all
I added canvas location parameters (x, y and lineHeight)
function wrapText(
ctx: CanvasRenderingContext2D,
text: string,
maxWidth: number,
x: number,
y: number,
lineHeight: number
) {
const xOffset = x;
let yOffset = y;
const lines = text.split('\n');
const fittingLines: [string, number, number][] = [];
for (let i = 0; i < lines.length; i++) {
if (ctx.measureText(lines[i]).width <= maxWidth) {
fittingLines.push([lines[i], xOffset, yOffset]);
yOffset += lineHeight;
} else {
let tmp = lines[i];
while (ctx.measureText(tmp).width > maxWidth) {
tmp = tmp.slice(0, tmp.length - 1);
}
if (tmp.length >= 1) {
const regex = new RegExp(`.{1,${tmp.length}}`, 'g');
const thisLineSplitted = lines[i].match(regex);
for (let j = 0; j < thisLineSplitted!.length; j++) {
fittingLines.push([thisLineSplitted![j], xOffset, yOffset]);
yOffset += lineHeight;
}
}
}
}
return fittingLines;
}
and you can just use this like
const wrappedText = wrapText(ctx, dialog, 200, 100, 200, 50);
wrappedText.forEach(function (text) {
ctx.fillText(...text);
});
}
Ok so I have this code for a JavaScript letter randomiser that works perfectly fine, i'm just having trouble figuring out how i'd get it to produce more than just one line while still keeping my code relatively efficient.
Ideally id like it to say something like this and then cycle back to the start:
Hi, my name is Yeet
this is my website
I like making cool stuff
take a look around :)
Any help would be greatly appreciated :)))
window.onload = function() {
var theLetters = "abcdefghijklmnopqrstuvwxyz#%&^+=-";
var cntnt = "Hi, my name is Yeet";
var speed = 20; // ms per frame
var increment = 2; // frames per step
var clen = cntnt.length;
var si = 0;
var stri = 0;
var block = "";
var fixed = "";
//Call self x times, whole function wrapped in setTimeout
(function rustle(i) {
setTimeout(function() {
if (--i) {
rustle(i);
}
nextFrame(i);
si = si + 1;
}, speed);
})(clen * increment + 1);
function nextFrame(pos) {
for (var i = 0; i < clen - stri; i++) {
var num = Math.floor(theLetters.length * Math.random());
//Get random letter
var letter = theLetters.charAt(num);
block = block + letter;
}
if (si == (increment - 1)) {
stri++;
}
if (si == increment) {
// Add a letter;
// every speed*10 ms
fixed = fixed + cntnt.charAt(stri - 1);
si = 0;
}
$("#output").html(fixed + block);
block = "";
}
};
Make cntnt an array
var cntnt = ["Hi, my name is Yeet", "This is my website", "I like making cool stuff", "take a look around :)"];
and use pos % cntnt.length as the array index.
fixed = fixed + cntnt[pos % cntnt.length].charAt(stri - 1);
I'm building a game currently but I have a small problem with keeping the score. Basically I have an textfield that gets a random word input, then if someone clicks on the field or symbol containing the field then I want to check the random word input, if it's a correct word I want to update score textfield, if it's incorrect I want to update errors textfield.
For this I am using an if/else construction, the problem I have with it is that in my game every click only goes either in the if statement or if I change code then only the else, but it's not checking symbol for symbol, every time I click to see if it's an correct word or not. Here is the code I am using on the symbol.click symbol. My question is, am I doing anything wrong in the if/else statements or are my variable calling methods wrong. I have source files on request.
symbol.click:
var y = sym.getVariable("lijst");
var x = "bommen";
// if variables are a match then update score with 1
if (sym.getVariable("x") == sym.getVariable("y"))
{var score3 = sym.getComposition().getStage().getVariable("score1");
score3= score3 +=1;
sym.getComposition().getStage().$ ("scoreTxt").html(score3);
sym.getComposition().getStage().setVariable("score1", score3);
}
// else update error textfield with 1
else {
var fouten= sym.getComposition().getStage().getVariable("fouten1");
fouten= fouten +=1;
sym.getComposition().getStage().$ ("hpTxt").html(fouten);
sym.getComposition().getStage().setVariable("fouten1", fouten);
}
symbol.creationComplete
var words = ['bommen',
'dammen',
'kanonnen',
'dollen',
'bomen',
'feesten',
'lampen',
'voeten',
];
var lijst = words[Math.floor(Math.random() * words.length)];
sym.$("dynamicText").html(lijst);
//And my stage:
stage.creationComplete
// some different variables declarations
sym.setVariable("score1", 0);
sym.setVariable("fouten1", 0)
//var game = sym.getComposition().getStage();
var cirkels = [];
var test1 = "bommen";
var score2 = sym.getComposition().getStage().getVariable("score1");
var fouten = sym.getComposition().getStage().getVariable("fouten1");
var cirkelStart = {x:180,y:190};
var cirkelSpacing = {x:170,y:170};
function init(){
initPlayer();
spawnCirkels();
}
//this is for score and error updating
function initPlayer(){
sym.$("scoreTxt").html(score2);
sym.$("hpTxt").html(fouten);
}
// create symbols on a grid
function spawnCirkels(){
var cirkel;
var el;
var i;
var xPos = cirkelStart.x;
var yPos = cirkelStart.y;
var col = 0;
for(i = 0;i < 15;i++){
cirkel = sym.createChildSymbol("Cirkel", "Stage");
cirkel.play(Math.random() * 1000);
cirkels.push(cirkel);
el = cirkel.getSymbolElement();
el.css({"position":"absolute", "top":yPos + "px", "left":xPos + "px"});
xPos += cirkelSpacing.x;
col++;
if(col === 5){
col = 0;
yPos += cirkelSpacing.y;
xPos = cirkelStart.x;
}
}
}
init();
If anyone sees what I am doing wrong let me know!
Thanks for your help anyway!
I am defining some global arrays at the top of my JS file. There are for loops to populate the arrays. I have done very similar things that have worked but for some reason, this is having issues. The first for loop runs through only one time and the code below that never gets ran.
var group = 'a';
var RoofVar = 1;
var TrimVar = 1;
var BBVar = 1;
var imageArrayGroup0 = [];
var floatingButtonArrayGroup0 = [];
var labelArrayGroup0 = [];
for (a = 1; a < 3; a++) {
labelArrayGroup0[a] = document.createElement("Label");
labelArrayGroup0[a].type = "Label";
floatingButtonArrayGroup0[a] = document.getElementById('floatingButton0' + (100 + a));
floatingButtonArrayGroup0[a].style.textAlign = "center";
floatingButtonArrayGroup0[a].style.paddingTop = "5px";
labelArrayGroup0[a] = document.getElementById('floatingButton0' + (100 + x));
}
var imageArray0 = [];
var floatingButtonArray0 = [];
var labelArray0 = [];
for (b = 1; b < 5; b++) {
imageArray0[b] = document.createElement("img");
imageArray0[b].type = "image";
floatingButtonArray0[b] = document.getElementById('floatingButton0' + (b));
floatingButtonArray0[b].style.paddingLeft = "15px";
floatingButtonArray0[b].style.paddingTop = "5px";
labelArray0[b] = document.getElementById('floatingButton0' + b);
}
var imageArray1 = [];
var floatingButtonArray1 = [];
var labelArray1 = [];
for (c = 1; c < 3; c++) {
imageArray1[c] = document.createElement("img");
imageArray1[c].type = "image";
floatingButtonArray1[c] = document.getElementById('floatingButton1' + (c));
floatingButtonArray1[c].style.paddingLeft = "15px";
floatingButtonArray1[c].style.paddingTop = "5px";
labelArray1[c] = document.getElementById('floatingButton1' + c);
}
You have other issues than just "(100 + x) instead of (100 + a).
First, arrays in JavaScript are "zero-based" but your loops start at 1.
This means you're putting things in but leaving spot '0' undefined, which will probably break things later.
Next. at the beginning of the loop you set labelArrayGroup0[a] to a new blank label, but you never do anything to put it on the page. At the end the loop, you overwrite labelArrayGroup0[a] to a floatingButton element you pull from the page.
In fact, you do this in every loop - the last line resets the value of the 1st line.
It's not clear what you're trying to accomplish. Presumably you intend to append labelArrayGroup0[a] to the page instead?
Also, in the middle, you set floatingButtonArrayGroup0[a] = document.getElementById('floatingButton0' + (100 + a)); which is the same element you grab in the last line of the loop...
Paul had my answer. I forgot to switch out the "x" for an "a".
I'm trying to create an animated sprite in cocos2d-js, but I don't want to use a spritesheet as I did in my cocos2d-iphone project:
NSMutableArray *animationFrames = [NSMutableArray array];
int frameCount = 0;
for(int i = 1; i <= 9; ++i)
{
CCSpriteFrame *spriteFrame = [CCSpriteFrame frameWithImageNamed:[NSString stringWithFormat:#"hero-%d.png",i]];
[animationFrames addObject:spriteFrame];
}
NSLog(#"cria sprite com frames");
_player = [CCSprite spriteWithSpriteFrame:animationFrames.firstObject];
How can I do this in cocos2d-js? I didn't find the same functions in cocos2d-js documentation.
Maybe following code is a little more complicated as it could be. But it works and loads sprites from files and puts together an animation and uses it in a runAction on a sprite (animFrame is an empty array, "this" is a ccLayer).
var str = "";
for (var i = 1; i < 9; i++) {
str = "mosquito_fly" + (i < 10 ? ("0" + i) : i) + ".png";
var texture = cc.textureCache.addImage("res/Murbiks/"+str);
var spriteFrame = cc.SpriteFrame.create(texture,cc.rect(0,0,96,96));
animFrames.push(spriteFrame);
}
var animation = cc.Animation.create(animFrames, 0.06+Math.random()*0.01, 10);
var animate = this.animateMostafa = cc.Animate.create(animation);
// Create sprite and set attributes
mostafa = cc.Sprite.create(res.Mostafa_single_png);
mostafa.attr({
x: 0,
y: 0,
scale: 0.60+Math.random()*0.3,
rotation: 0
});
this.addChild(mostafa, 0);