15 Commits
Author SHA1 Message Date
Vadim Namniak ffcaf3425c Update README.md 2015-01-25 15:27:49 -05:00
Vadim Namniak 365bd4e24c Update README.md 2015-01-25 15:27:19 -05:00
Vadim Namniak 96001e64df Update README.md 2015-01-25 15:26:14 -05:00
Vadim Namniak 49fea5c2bb Update README.md 2015-01-25 15:23:53 -05:00
Vadim Namniak 020d1dee52 Update README.md 2015-01-25 15:22:28 -05:00
Vadim Namniak c75fae5ea0 Update README.md 2015-01-25 15:18:39 -05:00
Vadim Namniak 9b2c460a69 v0.3.0 justifyLines, lineHeight and allowNewLine 2015-01-25 14:53:51 -05:00
Vadim Namniak 055cdaeb13 version updates 2014-10-12 11:04:53 -04:00
Vadim Namniak cfdccc0c61 version update 2014-10-12 11:03:27 -04:00
Vadim Namniak bb707cd508 Update README.md 2014-10-12 10:49:45 -04:00
Vadim Namniak 8dda2cb645 Update README.md 2014-10-12 10:37:29 -04:00
Vadim Namniak 7e3d425769 Update README.md 2014-10-12 01:57:56 -04:00
Vadim Namniak 6e03f629ce Merge pull request #3 from gmjosack/master
Add the ability to stroke text.
2014-10-12 01:32:44 -04:00
Gary M. Josack ebb8908812 Add the ability to stroke text.
This is useful in scenarios where a background is many colors.
2014-10-11 21:33:06 -07:00
Vadim Namniak 30c326ed36 Update README.md 2014-10-08 23:03:57 -04:00
6 changed files with 305 additions and 224 deletions
+2 -1
View File
@@ -1 +1,2 @@
examples examples/
.idea/
+208 -124
View File
@@ -1,6 +1,6 @@
/*! CanvasTextWrapper (https://github.com/namniak/CanvasTextWrapper) /*! CanvasTextWrapper
* Version: 0.2.0 * https://github.com/namniak/CanvasTextWrapper
* * Version: 0.3.0
* MIT License (http://www.opensource.org/licenses/mit-license.html) * MIT License (http://www.opensource.org/licenses/mit-license.html)
* Copyright (c) 2014 Vadim Namniak * Copyright (c) 2014 Vadim Namniak
*/ */
@@ -8,192 +8,276 @@
(function() { (function() {
'use strict'; 'use strict';
var defaultOptions = { var EL_WIDTH,EL_HEIGHT,MAX_TXT_WIDTH,MAX_TXT_HEIGHT;
var defaults = {
font: '18px Arial, sans-serif', font: '18px Arial, sans-serif',
sizeToFill: false, // text is resized to fill the container (given font size is ignored)
lineHeight: 1, // default line height equivalent of '100%'
allowNewLine: true, // breaks text on every new line character '\n'
lineBreak: 'auto', // text fills the element's (canvas or parent) width going to a new line on a whole word
textAlign: 'left', // each line of text is aligned left textAlign: 'left', // each line of text is aligned left
verticalAlign: 'top', // text lines block is aligned top verticalAlign: 'top', // text lines block is aligned top
paddingX: 0, // zero px left & right text padding relative to canvas or parent justifyLines: false, // lines are not justified
paddingY: 0, // zero px top & bottom text padding relative to canvas or parent paddingX: 0, // 0px left & right text padding relatively to canvas or its container
fitParent: false, // text is tested to fit canvas width paddingY: 0, // 0px top & bottom text padding relatively to canvas or its container
lineBreak: 'auto', // text fills the element's (canvas or parent) width going to a new line on a whole word fitParent: false, // text is set to fit canvas width
sizeToFill: false // text is resized to fill the container (given font size is ignored) strokeText: false // text is stroked according to context configuration
}; };
window.CanvasTextWrapper = function(canvas, text, opts) { var CanvasTextWrapper = function(canvas,text,options) {
if (!(this instanceof CanvasTextWrapper)) { if (!(this instanceof CanvasTextWrapper)) {
throw new TypeError('CanvasTextWrapper constructor failed. Use "new" keyword when instantiating.'); return new CanvasTextWrapper(canvas,text,options);
} }
this.canvas = canvas; this.canvas = canvas;
this.text = text; this.text = text;
// set options to specified or default values
for (var property in defaultOptions) {
this[property] = (opts && opts[property]) ? opts[property] : defaultOptions[property];
}
// extract font size
this.lineHeight = parseInt(this.font.replace(/^\D+/g, ''), 10) || 18;
// validate all set properties
this.validate();
// basic context settings
this.context = this.canvas.getContext('2d'); this.context = this.canvas.getContext('2d');
this.context.font = this.font; this.context.font = this.font;
this.context.textBaseline = 'bottom'; this.context.textBaseline = 'bottom';
this.drawText(); for (var property in defaults) {
if (defaults.hasOwnProperty(property)) {
this[property] = (options && options[property]) ? options[property] : defaults[property];
}
}
EL_WIDTH = (this.fitParent === false) ? this.canvas.width : this.canvas.parentNode.clientWidth;
EL_HEIGHT = (this.fitParent === false) ? this.canvas.height : this.canvas.parentNode.clientHeight;
MAX_TXT_WIDTH = EL_WIDTH - (this.paddingX * 2);
MAX_TXT_HEIGHT = EL_HEIGHT - (this.paddingY * 2);
this._init();
}; };
CanvasTextWrapper.prototype = { CanvasTextWrapper.prototype = {
_init: function() {
this.fontSize = parseInt(this.font.replace(/^\D+/g,''),10) || 18;
this.textBlockHeight = 0;
this.lines = [];
this.newLineIndexes = [];
this.textPos = {x: 0,y: 0};
drawText: function() { this._setFont(this.fontSize);
var elementWidth = (this.fitParent === false) ? this.canvas.width : this.canvas.parentNode.clientWidth; this._setLineHeight();
var textPos = { this._validate();
x: 0, this._render();
y: 0 },
};
_render: function() {
if (this.sizeToFill) { if (this.sizeToFill) {
// starting at 1px increase font size by 1px until text block exceeds the height of its padded container or until words break
var elementHeight = ((this.fitParent === false) ? this.canvas.height : this.canvas.parentNode.clientHeight) - (this.paddingX * 2);
var numWords = this.text.trim().split(/\s+/).length; var numWords = this.text.trim().split(/\s+/).length;
var fontSize = 0; var fontSize = 0;
do { do {
this.setFontSize(++fontSize); this._setFont(++fontSize);
var lines = this.getWrappedText(elementWidth); this.lineHeight = this.fontSize;
var textBlockHeight = lines.length * this.lineHeight; this._wrap();
} while (textBlockHeight < elementHeight && lines.join(' ').split(/\s+/).length == numWords); } while (this.textBlockHeight < MAX_TXT_HEIGHT && (this.lines.join(' ').split(/\s+/).length == numWords));
// use previous font size, not the one that broke the while condition this._setFont(--fontSize);
this.setFontSize(--fontSize); this.lineHeight = this.fontSize;
} else {
this._wrap();
} }
var lines = this.getWrappedText(elementWidth); if (this.justifyLines && this.lineBreak === 'auto') {
var textBlockHeight = lines.length * this.lineHeight; this._justify();
}
// set vertical align for the whole text block this._setAlignY();
this.setTextVerticalAlign(textPos, textBlockHeight); this._drawText();
},
for (var i = 0; i < lines.length; i++) { _setFont: function(fontSize) {
this.setTextHorizontalAlign(this.context, textPos, elementWidth, lines[i]); var fontParts = (!this.sizeToFill) ? this.font.split(/\b\d+px\b/i) : this.context.font.split(/\b\d+px\b/i);
this.context.font = fontParts[0] + fontSize + 'px' + fontParts[1];
this.fontSize = fontSize;
},
textPos.y = parseInt(textPos.y) + parseInt(this.lineHeight); _setLineHeight: function() {
this.context.fillText(lines[i], textPos.x, textPos.y); if (!isNaN(this.lineHeight)) {
this.lineHeight = this.fontSize * this.lineHeight;
} else if (this.lineHeight.toString().indexOf('px') !== -1) {
this.lineHeight = parseInt(this.lineHeight);
} else if (this.lineHeight.toString().indexOf('%') !== -1) {
this.lineHeight = (parseInt(this.lineHeight) / 100) * this.fontSize;
} }
}, },
setFontSize: function(size) { _wrap: function() {
var fontParts = this.context.font.split(/\b\d+px\b/i); if (this.allowNewLine) {
this.context.font = fontParts[0] + size + 'px' + fontParts[1]; var newLines = this.text.trim().split('\n');
this.lineHeight = size; for (var i = 0,idx = 0; i < newLines.length - 1; i++) {
}, idx += newLines[i].trim().split(/\s+/).length;
this.newLineIndexes.push(idx)
getWrappedText: function(elementWidth) { }
var maxTextLength = elementWidth - (this.paddingX * 2); }
var words = this.text.trim().split(/\s+/); var words = this.text.trim().split(/\s+/);
var lines = []; this._checkLength(words);
this._breakText(words);
this.checkWordsLength(this.context, words, maxTextLength); this.textBlockHeight = this.lines.length * this.lineHeight;
this.breakTextIntoLines(this.context, lines, words, maxTextLength);
return lines;
}, },
checkWordsLength: function(context, words, maxTextLength) { _checkLength: function(words) {
for (var i = 0; i < words.length; i++) { var testString,tokenLen,sliced,leftover;
var testString = '';
var tokenLen = context.measureText(words[i]).width;
// check if a word exceeds the element's width for (var i = 0; i < words.length; i++) {
if (tokenLen > maxTextLength) { testString = '';
for (var k = 0; (context.measureText(testString + words[i][k]).width <= maxTextLength) && (k < words[i].length); k++) { tokenLen = this.context.measureText(words[i]).width;
if (tokenLen > MAX_TXT_WIDTH) {
for (var k = 0; (this.context.measureText(testString + words[i][k]).width <= MAX_TXT_WIDTH) && (k < words[i].length); k++) {
testString += words[i][k]; testString += words[i][k];
} }
// break the word because it's too long sliced = words[i].slice(0,k);
var sliced = words[i].slice(0, k); leftover = words[i].slice(k);
var leftover = words[i].slice(k);
words.splice(i,1,sliced,leftover); words.splice(i,1,sliced,leftover);
} }
} }
}, },
breakTextIntoLines: function(context, lines, words, maxTextLength) { _breakText: function(words) {
for (var i = 0,j = 0; i < words.length; j++) { for (var i = 0,j = 0; i < words.length; j++) {
lines[j] = ''; this.lines[j] = '';
if (this.lineBreak === 'auto') { if (this.lineBreak === 'auto') {
// put as many full words in a line as can fit element while ((this.context.measureText(this.lines[j] + words[i]).width <= MAX_TXT_WIDTH) && (i < words.length)) {
while ((context.measureText(lines[j] + words[i]).width <= maxTextLength) && (i < words.length)) {
lines[j] += words[i] + ' '; this.lines[j] += words[i] + ' ';
i++; i++;
if (this.allowNewLine) {
for (var k = 0; k < this.newLineIndexes.length; k++) {
if (this.newLineIndexes[k] === i) {
j++;
this.lines[j] = '';
break;
} }
lines[j] = lines[j].trim(); }
} else if (this.lineBreak === 'word') { }
// put each next word in a new line }
lines[j] = words[i]; this.lines[j] = this.lines[j].trim();
} else {
this.lines[j] = words[i];
i++; i++;
} }
} }
}, },
setTextHorizontalAlign: function(context, textPos, elementWidth, line) { _justify: function() {
if (this.textAlign === 'center') { var maxLen,longestLineIndex,tokenLen;
textPos.x = (elementWidth - context.measureText(line).width) / 2; for (var i = 0; i < this.lines.length; i++) {
} else if (this.textAlign === 'right') { tokenLen = this.context.measureText(this.lines[i]).width;
textPos.x = elementWidth - context.measureText(line).width - this.paddingX;
} else { if (!maxLen || tokenLen > maxLen) {
textPos.x = this.paddingX; maxLen = tokenLen;
longestLineIndex = i;
}
}
// fill lines with extra spaces
var numWords,spaceLength,numOfSpaces,num,filler;
var delimiter = '\u200A';
for (i = 0; i < this.lines.length; i++) {
if (i === longestLineIndex) continue;
numWords = this.lines[i].trim().split(/\s+/).length;
if (numWords <= 1) continue;
this.lines[i] = this.lines[i].trim().split(/\s+/).join(delimiter);
spaceLength = this.context.measureText(delimiter).width;
numOfSpaces = (maxLen - this.context.measureText(this.lines[i]).width) / spaceLength;
num = numOfSpaces / (numWords - 1);
filler = '';
for (var j = 0; j < num; j++) {
filler += delimiter;
}
this.lines[i] = this.lines[i].trim().split(delimiter).join(filler);
//console.log('numWords:', numWords, 'numOfSpaces:', numOfSpaces, 'num:', num);
} }
}, },
setTextVerticalAlign: function(textPos, textBlockHeight) { _drawText: function() {
var elementHeight = (this.fitParent === false) ? this.canvas.height : this.canvas.parentNode.clientHeight; for (var i = 0; i < this.lines.length; i++) {
this._setAlignX(this.lines[i]);
if (this.verticalAlign === 'middle') { this.textPos.y = parseInt(this.textPos.y) + this.lineHeight;
textPos.y = (elementHeight - textBlockHeight) / 2; this.context.fillText(this.lines[i],this.textPos.x,this.textPos.y);
} else if (this.verticalAlign === 'bottom') {
textPos.y = elementHeight - textBlockHeight - this.paddingY; if (this.strokeText) {
} else { this.context.strokeText(this.lines[i],this.textPos.x,this.textPos.y);
textPos.y = this.paddingY; }
} }
}, },
validate: function() { _setAlignX: function(line) {
if (!(this.canvas instanceof HTMLCanvasElement)) { if (this.textAlign == 'center') {
throw new TypeError('From CanvasTextWrapper(): Element passed as the first parameter is not an instance of HTMLCanvasElement.'); this.textPos.x = (EL_WIDTH - this.context.measureText(line).width) / 2;
} else if (this.textAlign == 'right') {
this.textPos.x = EL_WIDTH - this.context.measureText(line).width - this.paddingX;
} else {
this.textPos.x = this.paddingX;
} }
if (typeof this.text !== 'string') { },
throw new TypeError('From CanvasTextWrapper(): The second, dedicated for the text, parameter must be a string.');
} _setAlignY: function() {
if (isNaN(this.lineHeight)) { if (this.verticalAlign == 'middle') {
throw new TypeError('From CanvasTextWrapper(): Cannot parse font size as an Integer. Check "font" property\'s value.'); this.textPos.y = (EL_HEIGHT - this.textBlockHeight) / 2;
} } else if (this.verticalAlign == 'bottom') {
if (this.textAlign !== 'left' && this.textAlign !== 'center' && this.textAlign !== 'right') { this.textPos.y = EL_HEIGHT - this.textBlockHeight - this.paddingY;
throw new TypeError('From CanvasTextWrapper(): Unsupported horizontal align value is used. Property "textAlign" can only be set to "left", "center", or "right".'); } else {
} this.textPos.y = this.paddingY;
if (this.verticalAlign !== 'top' && this.verticalAlign !== 'middle' && this.verticalAlign !== 'bottom') {
throw new TypeError('From CanvasTextWrapper(): Unsupported vertical align value is used. Property "verticalAlign" can only be set to "top", "middle", or "bottom".');
}
if (isNaN(this.paddingX)) {
throw new TypeError('From CanvasTextWrapper(): Unsupported horizontal padding value is used. Property "paddingX" must be set to a number');
}
if (isNaN(this.paddingY)) {
throw new TypeError('From CanvasTextWrapper(): Unsupported vertical padding value is used. Property "paddingY" must be set to a number.');
}
if (typeof this.fitParent !== 'boolean') {
throw new TypeError('From CanvasTextWrapper(): Property "fitParent" must be set to a Boolean.');
}
if (this.lineBreak !== 'auto' && this.lineBreak !== 'word') {
throw new TypeError('From CanvasTextWrapper(): Unsupported line break value is used. Property "lineBreak" can only be set to "auto", or "word".');
}
if (typeof this.sizeToFill !== 'boolean') {
throw new TypeError('From CanvasTextWrapper(): Property "sizeToFill" must be set to a Boolean.');
} }
},
_validate: function() {
if (!(this.canvas instanceof HTMLCanvasElement))
throw new TypeError('The first parameter must be an instance of HTMLCanvasElement.');
if (typeof this.text !== 'string')
throw new TypeError('The second parameter must be a string.');
if (isNaN(this.fontSize))
throw new TypeError('Cannot parse "font".');
if (isNaN(this.lineHeight))
throw new TypeError('Cannot parse "lineHeight".');
if (this.textAlign.toLocaleLowerCase() !== 'left' && this.textAlign.toLocaleLowerCase() !== 'center' && this.textAlign.toLocaleLowerCase() !== 'right')
throw new TypeError('Property "textAlign" must be set to either "left", "center", or "right".');
if (this.verticalAlign.toLocaleLowerCase() !== 'top' && this.verticalAlign.toLocaleLowerCase() !== 'middle' && this.verticalAlign.toLocaleLowerCase() !== 'bottom')
throw new TypeError('Property "verticalAlign" must be set to either "top", "middle", or "bottom".');
if (typeof this.justifyLines !== 'boolean')
throw new TypeError('Property "justifyLines" must be set to a Boolean.');
if (isNaN(this.paddingX))
throw new TypeError('Property "paddingX" must be set to a Number.');
if (isNaN(this.paddingY))
throw new TypeError('Property "paddingY" must be set to a Number.');
if (typeof this.fitParent !== 'boolean')
throw new TypeError('Property "fitParent" must be set to a Boolean.');
if (this.lineBreak.toLocaleLowerCase() !== 'auto' && this.lineBreak.toLocaleLowerCase() !== 'word')
throw new TypeError('Property "lineBreak" must be set to either "auto" or "word".');
if (typeof this.sizeToFill !== 'boolean')
throw new TypeError('Property "sizeToFill" must be set to a Boolean.');
if (typeof this.strokeText !== 'boolean')
throw new TypeError('Property "strokeText" must be set to a Boolean.');
} }
}; };
window.CanvasTextWrapper = CanvasTextWrapper;
})(); })();
+4 -4
View File
File diff suppressed because one or more lines are too long
+26 -30
View File
@@ -6,55 +6,51 @@ CanvasTextWrapper
new CanvasTextWrapper(HTMLCanvasElement, String [, options]); new CanvasTextWrapper(HTMLCanvasElement, String [, options]);
``` ```
```options``` - is a JavaScript object with the following available properties and values: ```options``` - is an object with the following available properties and values:
- ```font: String``` - text style that includes font size in px (REQUIRED), weight & family, similar to CSS font shorthand property - ```font:``` (String) - text style that includes font size in px, font weight, font family, etc. Similar to CSS font shorthand property
- ```textAlign: "left" | "center" | "right"``` - horizontal alignment that applies for each line - ```lineHeight:``` (String or Number) - Number means n times font size where 1 is equivalent to '100%'. Also the property can be set in "%" or "px" using String
- ```verticalAlign: "top" | "middle" | "bottom"``` - vertical alignment that applies on a whole block of text - ```textAlign: "left" | "center" | "right"``` - horizontal alignment of each line
- ```paddingX: Number``` - horizontal padding in pixels set equally on both, left and right sides of the element - ```verticalAlign: "top" | "middle" | "bottom"``` - vertical alignment of the whole text block
- ```paddingY: Number``` - vertical padding in pixels set equally on both, top and bottom sides of the element - ```paddingX:``` (Number) - horizontal padding (in px) set equally on both, left and right sides
- ```fitParent: Boolean``` - parameter that controls which element to fit where ```true``` means fit canvas parent's width instead of canvas own width - ```paddingY:``` (Number) - vertical padding (in px) set equally on both, top and bottom sides
- ```lineBreak: "auto" | "word"``` - text split rule. When using ```"auto"```, text fills the element's width going to a new line on a whole word when no more space. If ```"word"``` is set as value, each next word will be placed on a new line. - ```fitParent:``` (Boolean) - if enabled, text will fit canvas container's width instead of canvas own width
- ```sizeToFill: Boolean``` - ignore given font size and resize text to fill its padded container - ```lineBreak: "auto" | "word"``` - text split rule. When using ```"auto"```, text goes to a next line on a whole word when there's no more room. If ```"word"``` is set as value, each next word will be placed on a new line.
- ```sizeToFill:``` (Boolean) - ignore given font size and line height and resize text to fill its padded container
- ```strokeText:``` (Boolean) - outline text based on context configuration
- ```justifyLines:``` (Boolean) - if enabled, all lines match the same width with flexed spaces between words (one-word lines are ignored).
- ```allowNewLine:``` (Boolean) if enabled, the text breaks on every new line character "\n" otherwise it'll be considered as a space
NOTE: if a single word is too long to fit the width with specified font size, it will be broken into as many lines as required on any letter without specific word breaking rule. NOTE: if a single word is too long to fit the width with specified font size, it will break on any letter unless ```sizeToFill``` option is enabled.
##Defaults ##Defaults
The default options object which values will be used if a property is not specified or no object is passed:
``` ```
{ {
font: "18px Arial, sans-serif", font: "18px Arial, sans-serif",
lineHeight: 1,
textAlign: "left", textAlign: "left",
verticalAlign: "top", verticalAlign: "top",
paddingX: 0, paddingX: 0,
paddingY: 0, paddingY: 0,
fitParent: false, fitParent: false,
lineBreak: "auto", lineBreak: "auto",
sizeToFill: false strokeText: false
sizeToFill: false,
allowNewLine: true,
justifyLines: false
} }
``` ```
##Usage ##Usage
Use standard canvas text drawing methods such as "fillStyle" and "globalCompositeOperation" when needed before using CanvasTextWrapper like so: Configure context settings properties such as "fillStyle", "lineWidth" or "strokeStyle" before using CanvasTextWrapper like so:
``` ```
var canvas = document.createElement('canvas'); var canvas = document.getElementById("#canvasText");
canvas.width = 300; canvas.width = 200;
canvas.height = 250; canvas.height = 200;
context = canvas.getContext("2d"); context = canvas.getContext("2d");
context.fillStyle = "rgb(255, 255, 255)"; context.lineWidth = 2;
context.fillRect(0, 0, canvas.width, canvas.height); context.strokeStyle = "#ff0000";
CanvasTextWrapper(canvas,"Hello"); //default options will apply
context.globalCompositeOperation = "destination-out";
new CanvasTextWrapper(canvas, "Hi there", {
font: "normal 40px Open Sans, sans-serif",
textAlign: "center",
verticalAlign: "bottom",
paddingY: 10,
lineBreak: "word",
});
``` ```
##Examples ##Examples
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "canvas-text-wrapper", "name": "canvas-text-wrapper",
"version": "0.2.0", "version": "0.3.0",
"ignore": [ "ignore": [
"**/.*", "**/.*",
"**/*.log", "**/*.log",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "canvas-text-wrapper", "name": "canvas-text-wrapper",
"description": "JavaScript canvas text wrapper that automatically splits a string into lines on specified rule with optional alignments and padding.", "description": "Pure JavaScript canvas text wrapper that automatically splits a string into lines on specified rule with alignment and padding.",
"version": "0.2.0", "version": "0.3.0",
"license": "MIT", "license": "MIT",
"main": "CanvasTextWrapper.min.js", "main": "CanvasTextWrapper.min.js",
"homepage": "http://namniak.github.io/CanvasTextWrapper/", "homepage": "http://namniak.github.io/CanvasTextWrapper/",