我们知道使用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> |