用javascript结合HTML5画布(canvas)标签实现漂亮的两端对齐

我们知道使用css当中的text-align:justify就可以实现两端对齐了。比如下面这

<div style="text-align: justify; width: 300px">In olden times when wishing still helped one, there lived a king whose daughters were all beautiful; and the youngest was so beautiful that the sun itself, which has seen so much, was astonished whenever it shone in her face. Close by the king's castle lay a great dark forest, and under an old lime-tree in the forest was a well, and when the day was very warm, the king's child went out to the forest and sat down by the fountain; and when she was bored she took a golden ball, and threw it up on high and caught it; and this ball was her favorite plaything.</div>

In olden times when wishing still helped one, there lived a king whose daughters were all beautiful; and the youngest was so beautiful that the sun itself, which has seen so much, was astonished whenever it shone in her face. Close by the king’s castle lay a great dark forest, and under an old lime-tree in the forest was a well, and when the day was very warm, the king’s child went out to the forest and sat down by the fountain; and when she was bored she took a golden ball, and threw it up on high and caught it; and this ball was her favorite plaything.

我们知道,这种排版很类似Office Word的排版方式。如果有类似TEX的排版,对于这些非等宽字体,就会更好看了。Bram Stein就为我们用javascript实现了这样的功能。

比较我们可以发现,使用javascript的话,排版会更紧凑。其实它更强大之处还在于图文混排。当然那需要更强的图层控制能力。

<div><canvas id="justify" width="350px" height="190"></canvas></div>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.4.2.min.js"></script>
<script type="text/javascript">
<!--
/*!
 * JavaScript Core Object v0.53
 *
 * Licensed under the new BSD License.
 * Copyright 2008-2009, Bram Stein
 * All rights reserved.
 */
(function () {
	function getInternalType(value) {
		return Object.prototype.toString.apply(value);
	}
 
    function Clone() {}
 
	Object.extend = function (obj) {
		var i = 1, key, len = arguments.length;
		for (; i < len; i += 1) {
			for (key in arguments[i]) {
				// make sure we do not override built-in methods but toString and valueOf
				if (arguments[i].hasOwnProperty(key) && 
					(!obj[key] || obj.propertyIsEnumerable(key) || key === 'toString' || key === 'valueOf')) {
					obj[key] = arguments[i][key];
				}
			}
		}
		return obj;
	};
 
	Object.extend(Object, {
		isAtom: function (value) {
			return ((typeof value !== 'object' || value === null) && 
				typeof value !== 'function') || 
				Object.isBoolean(value) || Object.isNumber(value) || Object.isString(value);
		},
 
		isNumber: function (value) {
			return (typeof value === 'number' || value instanceof Number) && !isNaN(value);
		},
 
		isString: function (value) {
			return typeof value === 'string' || value instanceof String;
		},
 
		isBoolean: function (value) {
			return value !== null && 
				(typeof value === 'boolean' || value instanceof Boolean);
		},
 
		isArray: function (value) {
			return getInternalType(value) === '[object Array]';
		},
 
		isObject: function (value) {
			return getInternalType(value) === '[object Object]';
		},
 
		isFunction: function (value) {
			return typeof value === 'function';
		},
 
		isDefined: function (value) {
			return typeof value !== 'undefined';
		},
 
		filter: function (obj, fun, thisObj) {
			var key, r = {}, val;
			thisObj = thisObj || obj;
			for (key in obj) {
				if (obj.hasOwnProperty(key)) {
					val = obj[key];
					if (fun.call(thisObj, val, key, obj)) {
						r[key] = val;
					}
				}
			}
			return r;
		},
 
		map: function (obj, fun, thisObj) {
			var key, r = {};
			thisObj = thisObj || obj;
			for (key in obj) {
				if (obj.hasOwnProperty(key)) {
					r[key] = fun.call(thisObj, obj[key], key, obj);
				}
			}
			return r;
		},
 
		forEach: function (obj, fun, thisObj) {
			var key;
			thisObj = thisObj || obj;
			for (key in obj) {
				if (obj.hasOwnProperty(key)) {
					fun.call(thisObj, obj[key], key, obj);			
				}
			}
		},
 
		every: function (obj, fun, thisObj) {
			var key;
			thisObj = thisObj || obj;
			for (key in obj) {
				if (obj.hasOwnProperty(key) && !fun.call(thisObj, obj[key], key, obj)) {
					return false;
				}
			}
			return true;
		},
 
		some: function (obj, fun, thisObj) {
			var key;
			thisObj = thisObj || obj;
			for (key in obj) {
				if (obj.hasOwnProperty(key) && fun.call(thisObj, obj[key], key, obj)) {
					return true;
				}
			}
			return false;
		},
 
		isEmpty: function (obj) {
			return Object.every(obj, function (value, key) { 
				return !obj.hasOwnProperty(key); 
			});
		},
 
		values: function (obj) {
			var r = [];
			Object.forEach(obj, function (value) {
				r.push(value);
			});
			return r;
		},
 
		keys: function (obj) {
			var r = [];
			Object.forEach(obj, function (value, key) {
				r.push(key);
			});
			return r;
		},
 
        // Shallow or deep copy of an object. Code inspired by:
        // * Oran Looney - http://oranlooney.com/static/functional_javascript/owl_util.js
        // * Object-Oriented JavaScript, by Stoyan Stefanov
		copy: function (obj, deep) {
            var c, p, r;
 
            if (typeof obj !== 'object') {
                return obj;
            } else {
                c = obj.valueOf();
 
                // Test for strict identity: if they are not equal we 
                // can be sure this not a native type wrapper.
                if (obj !== c) {
                    return new obj.constructor(c);
                }
 
                // We clone the prototype if possible, otherwise construct a clean object or array
                if (obj instanceof obj.constructor && obj.constructor !== Object && !Object.isArray(obj)) {
                    r = Object.clone(obj.constructor.prototype);
                } else {
                    r = Object.isArray(obj) ? [] : {};
                }
 
                for (p in obj) {
                    if (obj.hasOwnProperty(p)) {
                        r[p] = deep ? Object.copy(obj[p], deep) : obj[p]; 
                    }
                }
                return r;
            }
		},
 
		clone: function (obj) {
			Clone.prototype = obj;
			return new Clone();
		},
 
		reduce: function (obj, fun, initial) {
			var key, initialKey;
 
			if (Object.isEmpty(obj) && initial === undefined) {
				throw new TypeError();
			}
			if (initial === undefined) {
				for (key in obj) {
					if (obj.hasOwnProperty(key)) {
						initial = obj[key];
						initialKey = key;
						break;
					}
				}
			}
			for (key in obj) {
				if (obj.hasOwnProperty(key) && key !== initialKey) {
					initial = fun.call(null, initial, obj[key], key, obj);
				}
			}
			return initial;
		}
	});
})();
 
/*!
 * JavaScript Core Array v0.39
 *
 * Licensed under the new BSD License.
 * Copyright 2008-2009, Bram Stein
 * All rights reserved.
 */
(function () {
	Object.extend(Array.prototype, {
		isEmpty: function () {
			return this.length < 1;
		},
		append: function () {
			var i = 0, len = arguments.length;
			// interestingly enough, push() beats both
			// concat()---which was expected---and splice()
			for (; i < len; i += 1) {
				this.push.apply(this, arguments[i]);
			}
			return this;
		},
		peek: function () {
			return this[this.length - 1];
		},
		contains: function (v) {
			return this.indexOf(v) !== -1;
		}
	});
 
	['reduce', 'reduceRight', 'filter', 'map', 'forEach', 'some', 'every', 'indexOf', 'lastIndexOf', 'isEmpty', 'equals', 'contains', 'append', 'peek', 'join', 'sort', 'reverse', 'push', 'pop', 'shift', 'unshift', 'splice', 'concat', 'slice'].forEach(function (func) {
		if (!(func in Array) && func in Array.prototype) {
			Array[func] = function (obj) {
				return this.prototype[func].apply(obj, Array.prototype.slice.call(arguments, 1));
			};
		}
	});
})();
 
function LinkedList() {
	this.head = null;
	this.tail = null;
	this.listSize = 0;
}
(function () {
	Object.extend(LinkedList, {
		Node: function (data) {
			this.prev = null;
			this.next = null;
			this.data = data;		
		}
	});
 
	Object.extend(LinkedList.Node.prototype, {
		toString: function () {
			return this.data.toString();
		}
	});
 
	function isLinked(list, node) {
		return !((node && node.prev === null && node.next === null && list.tail !== node && list.head !== node) || list.isEmpty());
	}
 
	Object.extend(LinkedList.prototype, {
		size: function () {
			return this.listSize;
		},
 
		isEmpty: function () {
			return this.listSize === 0;
		},
 
		first: function () {
			return this.head;
		},
 
		last: function () {
			return this.last;
		},
 
		toString: function () {
			return this.toArray().toString();
		},
 
		toArray: function () {
			var node = this.head,
				result = [];
			while (node !== null) {
				result.push(node);
				node = node.next;
			}
			return result;
		},
 
		// Note that modifying the list during
		// iteration is not safe.
		forEach: function (fun) {
			var node = this.head;
			while (node !== null) {
				fun(node);
				node = node.next;
			}
		},	
 
		contains: function (n) {
			var node = this.head;
			if (!isLinked(this, n)) {
				return false;
			}
			while (node !== null) {
				if (node === n) {
					return true;
				}
				node = node.next;
			}
			return false;
		},
 
		at: function (i) {
			var node = this.head, index = 0;
 
			if (i >= this.listLength || i < 0) {
				return null;
			}
 
			while (node !== null) {
				if (i === index) {
					return node;
				}
				node = node.next;
				index += 1;
			}
			return null;
		},
 
		insertAfter: function (node, newNode) {
			if (!isLinked(this, node)) {
				return this;
			}
			newNode.prev = node;
			newNode.next = node.next;
			if (node.next === null) {
				this.tail = newNode;
			} else {
				node.next.prev = newNode;
			}
			node.next = newNode;
			this.listSize += 1;
			return this;
		},
 
		insertBefore: function (node, newNode) {
			if (!isLinked(this, node)) {
				return this;
			}
			newNode.prev = node.prev;
			newNode.next = node;
			if (node.prev === null) {
				this.head = newNode;
			} else {
				node.prev.next = newNode;
			}
			node.prev = newNode;
			this.listSize += 1;
			return this;
		},
 
		push: function (node) {
			if (this.head === null) {
				this.unshift(node);
			} else {
				this.insertAfter(this.tail, node);
			}
			return this;
		},
 
		unshift: function (node) {
			if (this.head === null) {
				this.head = node;
				this.tail = node;
				node.prev = null;
				node.next = null;
				this.listSize += 1;
			} else {
				this.insertBefore(this.head, node);
			}
			return this;
		},
 
		remove: function (node) {
			if (!isLinked(this, node)) {
				return this;
			}
			if (node.prev === null) {
				this.head = node.next;
			} else {
				node.prev.next = node.next;
			}
			if (node.next === null) {
				this.tail = node.prev;
			} else {
				node.next.prev = node.prev;
			}
			this.listSize -= 1;
			return this;
		},
 
		pop: function () {
			var node = this.tail;
			this.tail.prev.next = null;
			this.tail = this.tail.prev;
			this.listSize -= 1;
			node.prev = null;
			node.next = null;
			return node;
		},
 
		shift: function () {
			var node = this.head;
			this.head.next.prev = null;
			this.head = this.head.next;
			this.listSize -= 1;
			node.prev = null;
			node.next = null;
			return node;
		}
	});
}());
 
 
/*global LinkedList*/
/*requires ../core/object.js*/
/*requires ../core/array.js*/
/*requires ../core/linked-list.js*/
 
/*!
 * Knuth and Plass line breaking algorithm in JavaScript
 *
 * Licensed under the new BSD License.
 * Copyright 2009-2010, Bram Stein
 * All rights reserved.
 */
var linebreak = function (nodes, lines, settings) {
	var options = Object.extend({}, linebreak.defaults, settings),
		activeNodes = new LinkedList(),
		sum = {
			width: 0,
			stretch: 0,
			shrink: 0
		},
		lineLengths = lines,
		breaks = [],
		tmp = {
			data: {
				demerits: Infinity
			}
		};
 
	function breakpoint(position, demerits, ratio, line, fitnessClass, totals, previous) {
		return {
			position: position,
			demerits: demerits,
			ratio: ratio,
			line: line,
			fitnessClass: fitnessClass,
			totals: totals || {
				width: 0,
				stretch: 0,
				shrink: 0
			},
			previous: previous
		};
	}
 
	function computeCost(start, end, active, currentLine) {
		var width = sum.width - active.totals.width,
			stretch = 0,
			shrink = 0,
			// If the current line index is within the list of linelengths, use it, otherwise use
			// the last line length of the list.
			lineLength = currentLine < lineLengths.length ? lineLengths[currentLine - 1] : lineLengths[lineLengths.length - 1];
 
		if (nodes[end].type === 'penalty') {
			width += nodes[end].width;
		}
 
		if (width < lineLength) {
			// Calculate the stretch ratio
			stretch = sum.stretch - active.totals.stretch;
 
			if (stretch > 0) {
				return (lineLength - width) / stretch;
			} else {
				return options.infinity;
			}
 
		} else if (width > lineLength) {
			// Calculate the shrink ratio
			shrink = sum.shrink - active.totals.shrink;
 
			if (shrink > 0) {
				return (lineLength - width) / shrink;
			} else {
				return options.infinity;
			}
		} else {
			// perfect match
			return 0;
		}
	}
 
 
	// Add width, stretch and shrink values from the current 
	// break point up to the next box or forced penalty.
	function computeSum(breakPointIndex) {
		var result = {
				width: sum.width,
				stretch: sum.stretch,
				shrink: sum.shrink
			},
			i = 0;
 
		for (i = breakPointIndex; i < nodes.length; i += 1) {
			if (nodes[i].type === 'glue') {
				result.width += nodes[i].width;
				result.stretch += nodes[i].stretch;
				result.shrink += nodes[i].shrink;
			} else if (nodes[i].type === 'box' || (nodes[i].type === 'penalty' && nodes[i].penalty === -options.infinity && i > breakPointIndex)) {
				break;
			}
		}
		return result;
	}
 
	// The main loop of the algorithm
	function mainLoop(node, index, nodes) {
		var active = activeNodes.first(),
			next = null,
			ratio = 0,
			demerits = 0,
			candidates = [],
			badness,
			currentLine = 0,
			tmpSum,
			currentClass = 0;
 
		// The inner loop iterates through all the active nodes with line < currentLine and then
		// breaks out to insert the new active node candidates before looking at the next active
		// nodes for the next lines. The result of this is that the active node list is always
		// sorted by line number.
		while (active !== null) {
 
			candidates = [{demerits: Infinity}, {demerits: Infinity}, {demerits: Infinity}, {demerits: Infinity}];
 
			// Iterate through the linked list of active nodes to find new potential active nodes 
			// and deactivate current active nodes.
			while (active !== null) {
				next = active.next;
				currentLine = active.data.line + 1;
				ratio = computeCost(active.data.position, index, active.data, currentLine);
 
				// Deactive nodes when the the distance between the current active node and the
				// current node becomes too large (i.e. it exceeds the stretch limit and the stretch
				// ratio becomes negative) or when the current node is a forced break (i.e. the end
				// of the paragraph when we want to remove all active nodes, but possibly have a final
				// candidate active node---if the paragraph can be set using the given tolerance value.)
				if (ratio < -1 || (node.type === 'penalty' && node.penalty === -options.infinity)) {
					activeNodes.remove(active);
				}
 
				// If the ratio is within the valid range of -1 <= ratio <= tolerance calculate the 
				// total demerits and record a candidate active node.
				if (-1 <= ratio && ratio <= options.tolerance) {
					badness = 100 * Math.pow(Math.abs(ratio), 3);
 
					// Positive penalty
					if (node.type === 'penalty' && node.penalty >= 0) {
						demerits = Math.pow(options.demerits.line + badness + node.penalty, 2);
					// Negative penalty but not a forced break
					} else if (node.type === 'penalty' && node.penalty !== -options.infinity) {
						demerits = Math.pow(options.demerits.line + badness - node.penalty, 2);
					// All other cases
					} else {
						demerits = Math.pow(options.demerits.line + badness, 2);
					}
 
					if (node.type === 'penalty' && nodes[active.data.position].type === 'penalty') {
						demerits += options.demerits.flagged * node.flagged * nodes[active.data.position].flagged;
					}
 
					// Calculate the fitness class for this candidate active node.
					if (ratio < -0.5) {
						currentClass = 0;
					} else if (ratio <= 0.5) {
						currentClass = 1;
					} else if (ratio <= 1) {
						currentClass = 2;
					} else {
						currentClass = 3;
					}
 
					// Add a fitness penalty to the demerits if the fitness classes of two adjacent lines
					// differ too much.
					if (Math.abs(currentClass - active.data.fitnessClass) > 1) {
						demerits += options.demerits.fitness;
					}
 
					// Add the total demerits of the active node to get the total demerits of this candidate node.
					demerits += active.data.demerits;
 
					// Only store the best candidate for each fitness class
					if (demerits < candidates[currentClass].demerits) {
						candidates[currentClass] = {active: active, demerits: demerits, ratio: ratio};
					}
				}
 
				active = next;
 
				// Stop iterating through active nodes to insert new candidate active nodes in the active list
				// before moving on to the active nodes for the next line.
				// TODO: The Knuth and Plass paper suggests a conditional for currentLine < j0. This means paragraphs
				// with identical line lengths will not be sorted by line number. Find out if that is a desirable outcome.
				// For now I left this out, as it only adds minimal overhead to the algorithm and keeping the active node
				// list sorted has a higher priority.
				if (active !== null && active.data.line >= currentLine) {
					break;
				}
			}
 
			tmpSum = computeSum(index);
 
			candidates.forEach(function (candidate, fitnessClass) {
				var newNode;
 
				if (candidate.demerits < Infinity) {
					newNode = new LinkedList.Node(breakpoint(index, candidate.demerits, candidate.ratio, 
													candidate.active.data.line + 1, fitnessClass, tmpSum, candidate.active));
					if (active !== null) {
						activeNodes.insertBefore(active, newNode);
					} else {
						activeNodes.push(newNode);
					}
				}
			});
		}
	}
 
	// Add an active node for the start of the paragraph.
	activeNodes.push(new LinkedList.Node(breakpoint(0, 0, 0, 0, 0, undefined, null)));
 
	nodes.forEach(function (node, index, nodes) {
		if (node.type === 'box') {
			sum.width += node.width;
		} else if (node.type === 'glue') {
			if (index > 0 && nodes[index - 1].type === 'box') {
				mainLoop(node, index, nodes);
			}
			sum.width += node.width;
			sum.stretch += node.stretch;
			sum.shrink += node.shrink;
		} else if (node.type === 'penalty' && node.penalty !== options.infinity) {
			mainLoop(node, index, nodes);
		}
	});
 
 
	if (activeNodes.size() !== 0) {
		// Find the best active node (the one with the least total demerits.)
		activeNodes.forEach(function (node) {
			if (node.data.demerits < tmp.data.demerits) {
				tmp = node;
			}
		});
 
		while (tmp !== null) {
			breaks.push({position: tmp.data.position, ratio: tmp.data.ratio});
			tmp = tmp.data.previous;
		}
		return breaks.reverse();
	}
	return [];
};
 
Object.extend(linebreak, {
	defaults: {
		demerits: {
			line: 10,
			flagged: 100,
			fitness: 3000
		},
		infinity: 10000,
		tolerance: 2
	},
	glue: function (width, stretch, shrink) {
		return {
			type: 'glue',
			width: width,
			stretch: stretch,
			shrink: shrink
		};
	},
	box: function (width, value) {
		return {
			type: 'box',
			width: width,
			value: value
		};
	},
	penalty: function (width, penalty, flagged) {
		return {
			type: 'penalty',
			width: width,
			penalty: penalty,
			flagged: flagged
		};
	}
});
 
/*global linebreak*/
/*requires ../core/object.js*/
/*requires ../core/array.js*/
/*requires linebreak.js*/
 
/*!
 * Knuth and Plass line breaking algorithm in JavaScript
 *
 * Licensed under the new BSD License.
 * Copyright 2009-2010, Bram Stein
 * All rights reserved.
 */
var formatter = function (measureText, options) {
	var spaceWidth = measureText(' '),
		o = Object.extend({}, formatter.defaults, options);
	//console.log(emWidth);
	//emWidth = 12;
 
	return {
		center: function (text) {
			var nodes = [],
				words = text.split(/\s/),
				spaceStretch = (spaceWidth * o.space.width) / o.space.stretch,
				spaceShrink = (spaceWidth * o.space.width) / o.space.shrink;
 
			// Although not specified in the Knuth and Plass whitepaper, this box is necessary
			// to keep the glue from disappearing.
			nodes.push(linebreak.box(0, ''));
			nodes.push(linebreak.glue(0, 12, 0));
 
			words.forEach(function (word, index, array) {
				nodes.push(linebreak.box(measureText(word), word));
 
				if (index === array.length - 1) {
					nodes.push(linebreak.glue(0, 12, 0));
					nodes.push(linebreak.penalty(0, -linebreak.defaults.infinity, 0));
				} else {
					nodes.push(linebreak.glue(0, 12, 0));
					nodes.push(linebreak.penalty(0, 0, 0));
					nodes.push(linebreak.glue(spaceWidth, -24, 0));
					nodes.push(linebreak.box(0, ''));
					nodes.push(linebreak.penalty(0, linebreak.defaults.infinity, 0));
					nodes.push(linebreak.glue(0, 12, 0));
				}
			});
			return nodes;
		},
		justify: function (text) {
			var nodes = [],
				words = text.split(/\s/),
				spaceStretch = (spaceWidth * o.space.width) / o.space.stretch,
				spaceShrink = (spaceWidth * o.space.width) / o.space.shrink;
 
			words.forEach(function (word, index, array) {
				nodes.push(linebreak.box(measureText(word), word));
 
				if (index === array.length - 1) {
					nodes.push(linebreak.glue(0, linebreak.defaults.infinity, 0));
					nodes.push(linebreak.penalty(0, -linebreak.defaults.infinity, 1)); 
				} else {
					nodes.push(linebreak.glue(spaceWidth, spaceStretch, spaceShrink));
				}
			});
			return nodes;
		},
		left: function (text) {
			var nodes = [],
				words = text.split(/\s/),
				spaceStretch = (spaceWidth * o.space.width) / o.space.stretch,
				spaceShrink = (spaceWidth * o.space.width) / o.space.shrink;
 
			words.forEach(function (word, index, array) {
				nodes.push(linebreak.box(measureText(word), word));
 
				if (index === array.length - 1) {
					nodes.push(linebreak.glue(0, linebreak.defaults.infinity, 0));
					nodes.push(linebreak.penalty(0, -linebreak.defaults.infinity, 1)); 
				} else {
					nodes.push(linebreak.glue(0, 12, 0));
					nodes.push(linebreak.penalty(0, 0, 0));
					nodes.push(linebreak.glue(spaceWidth, -12, 0)); 
				}
			});
			return nodes;
		}
	};
};
 
Object.extend(formatter, {
	defaults: {
		space: {
			width: 3,
			stretch: 6,
			shrink: 9
		}
	}
});
 
var text = "In olden times when wishing still helped one, there lived a king whose daughters were all beautiful; and the youngest was so beautiful that the sun itself, which has seen so much, was astonished whenever it shone in her face. Close by the king's castle lay a great dark forest, and under an old lime-tree in the forest was a well, and when the day was very warm, the king's child went out to the forest and sat down by the fountain; and when she was bored she took a golden ball, and threw it up on high and caught it; and this ball was her favorite plaything."
 
function draw(context, nodes, breaks, lineLengths, drawRatio, center) {
				var i = 0, lines = [], point, j, r, lineStart = 0, y = 4, tmp, maxLength = Math.max.apply(null, lineLengths);
 
				// Iterate through the line breaks, and split the nodes at the
				// correct point.
				for (i = 1; i < breaks.length; i += 1) {
					point = breaks[i].position,
					r = breaks[i].ratio;
 
					for (var j = lineStart; j < nodes.length; j += 1) {
						// After a line break, we skip any nodes unless they are boxes or forced breaks.
						if (nodes[j].type === 'box' || (nodes[j].type === 'penalty' && nodes[j].penalty === -linebreak.defaults.infinity)) {
							lineStart = j;
							break;
						}
					}
					lines.push({ratio: r, nodes: nodes.slice(lineStart, point + 1), position: point});
					lineStart = point;
				}
 
				lines.forEach(function (line, lineIndex) {
					var x = 0, lineLength = lineIndex < lineLengths.length ? lineLengths[lineIndex] : lineLengths[lineLengths.length - 1];
 
					if (center) {
						x += (maxLength - lineLength) / 2;
 
					}
 
					line.nodes.forEach(function (node, index) {
						if (node.type === 'box') {
							context.fillText(node.value, x, y);
							x += node.width;
						} else if (node.type === 'glue') {
							x += node.width + line.ratio * (line.ratio < 0 ? node.shrink : node.stretch);
						}
					});
 
					if (drawRatio) {
						context.textAlign = 'right';
						context.fillText(line.ratio.toFixed(3), context.canvas.width, y);
						context.textAlign = 'left';
					}
 
					y += 21;
				});
				return lines;
			}
 
jQuery(function ($) {
				function align(identifier, type, lineLengths, tolerance, drawRatio, center) {
					var canvas = $(identifier).get(0),
						context = canvas.getContext && canvas.getContext('2d'),
						format, nodes, breaks;
 
					if (context) {
						context.textBaseline = 'top';
						context.font = "14px 'times new roman', 'FreeSerif', serif";
 
						format = formatter(function (str) {
							return context.measureText(str).width;
						});
 
						nodes = format[type](text);
 
						breaks = linebreak(nodes, lineLengths, {tolerance: tolerance});
 
						if (!breaks.isEmpty()) {
							return draw(context, nodes, breaks, lineLengths, drawRatio, center);
						} else {
							context.fillText('Paragraph can not be set with the given tolerance.', 0, 0);
						}
					}
					return [];
				}
 
				align('#justify', 'justify', [300], 3, false, true);
			});
-->
</script>

发表评论

电子邮件地址不会被公开。 必填项已用*标注