From 50f89a6e65fba3665dce07ff7be91926ae3877a7 Mon Sep 17 00:00:00 2001 From: Victor Michnowicz Date: Tue, 28 Oct 2014 22:11:15 -0400 Subject: [PATCH 1/2] Add bitmap text plugin --- src/plugins/BitmapText.js | 281 +++++++++++++++++++++++++++ test/unit/plugins/BitmapText-test.js | 43 ++++ 2 files changed, 324 insertions(+) create mode 100644 src/plugins/BitmapText.js create mode 100644 test/unit/plugins/BitmapText-test.js diff --git a/src/plugins/BitmapText.js b/src/plugins/BitmapText.js new file mode 100644 index 00000000..f8a51f71 --- /dev/null +++ b/src/plugins/BitmapText.js @@ -0,0 +1,281 @@ +(function() { + + var ATTR_CHANGE_LIST = ['text', 'image', 'chars', 'charSpacing'], + CHANGE_KINETIC = 'Change.kinetic', + BITMAPTEXT = 'BitmapText', + STANDARD = 'standard', + + // cached variables + attrChangeListLen = ATTR_CHANGE_LIST.length; + + /** + * Bitmap text constructor + */ + Kinetic.BitmapText = function(config) { + config = config || {}; + this.____init(config); + }; + + Kinetic.BitmapText.prototype = { + ____init : function(config) { + var self = this; + + Kinetic.Shape.call(this, config); // This shape will act as a Kinetic group + + self.className = BITMAPTEXT; + self.attrs.cgaCache = {}; // Empty object for cache + + this._addListeners(); + + this._setTextData(); + + this.sceneFunc(this._sceneFunc); + this.hitFunc(this._hitFunc); + }, + _setTextData: function() { + this.textData = []; + this.lineWidths = [0]; + + var text = this.attrs.text, + el = document.createElement('div'); + + // Empty string values + if (typeof text === 'undefined' || text === null || isNaN(text)) { + text = ''; + } + + el.innerHTML = text; + + var nodes = el.childNodes, + padding = this.getPadding() || 0, + x = padding, + y = padding, + charSpacing = typeof this.attrs.charSpacing === 'number' ? this.attrs.charSpacing : 0, + i = 0, + len = nodes.length, + width = null, + height = null, + line = 0, + lineHeight = this.getLineHeight(); + + for (; i < len; i++) { + var node = nodes[i], + tag = node.tagName ? node.tagName.toLowerCase() : STANDARD, + text = String(node.textContent); + + // New line + if (tag === 'br') { + line++; + this.lineWidths.push(0); + + x = 0; + y += lineHeight; + } + + // Loop through each character + for (var c = 0; c < text.length; c++) { + + var char = text.charAt(c), + pos = this._getPos(char, tag); + + this.textData.push({ + char: char, + tag: tag, + sx: pos.x, // Source X + sy: pos.y, // Source Y + sw: pos.width, // Source width + sh: pos.height, // Source height + dx: x, // Destination X + dy: y, // Destination Y + dw: pos.width, // Destination width + dh: pos.height // Destination height + }); + + x += pos.width + charSpacing; + + // Update line width + this.lineWidths[this.lineWidths.length - 1] = x; + } + } + + width = x - charSpacing + padding; + height = ( (this.lineWidths.length + 1) * line) + (padding * 2); + + this.setAttrs({ + width: width, + height: height + }); + }, + getLineHeight: function() { + return typeof this.attrs.lineHeight === 'number' ? this.attrs.lineHeight : this._getLineHeight(); + }, + setLineHeight: function(lineHeight) { + this.attrs.lineHeight = lineHeight; + }, + /** + * Get line height for a specific tag + * + * @param {String} tag + * @return {Number} + * @private + */ + _getLineHeight: function(tag) { + + tag = tag || STANDARD; + + var height = 0; + + if (typeof this.attrs.chars === 'object') { + if (typeof this.attrs.chars[tag]['1'] === 'object' ) { + height = this.attrs.chars[tag]['1'][3]; + } + + if (typeof this.attrs.chars[tag]['T'] === 'object' ) { + height = Math.max(height, this.attrs.chars[tag]['1'][3]); + } + + if (typeof this.attrs.chars[tag]['L'] === 'object' ) { + height = Math.max(this.attrs.chars[tag]['1'][3]); + } + + // If we could not determine a line height + if (height === 0) { + // Loop through each tag type get first character with a height value + for (var t in this.attrs.chars) { + // If tag has object of chars + if ( typeof this.attrs.chars[t] === 'object' ) { + // Loop through each char in this tag + for (var c in this.attrs.chars[t]) { + // If this char has a height + if ( typeof this.attrs.chars[t][c][3] === 'number' ) { + return this.attrs.chars[t][c][3]; + } + } + } + } + } + } + + return height; + }, + /** + * Get position and dimensions of a character + * + * @param char + * @param tag + * @returns {{x: number, y: number, width: number, height: number}} + * @private + */ + _getPos: function(char, tag) { + var temp = null, + pos = { + x: 0, + y: 0, + width: 0, + height: 0 + }; + + if (typeof this.attrs.chars === 'object') { + // If this tag exists and this character exists with this tag + if (this.attrs.chars[tag] && typeof this.attrs.chars[tag][char] === 'object') { + temp = this.attrs.chars[tag][char]; + } + // Else, if this character exists with a standard tag + else if (typeof this.attrs.chars[STANDARD][char] === 'object') { + temp = this.attrs.chars[STANDARD][char]; + } + } + + if (temp) { + pos.x = temp[0]; + pos.y = temp[1]; + pos.width = temp[2]; + pos.height = temp[3]; + } + + return pos; + + }, + _addListeners: function() { + var self = this; + + // Update on certain attr changes + for (var n = 0; n < attrChangeListLen; n++) { + self.on(ATTR_CHANGE_LIST[n] + CHANGE_KINETIC, function() { + self._setTextData(); + }); + } + }, + _sceneFunc: function(context) { + var image = this.attrs.image || new Image(), + i = 0, + len = this.textData.length; + + // If we have a custom fill + if (this.attrs.fill) { + + var fillCanvas = document.createElement('canvas'), + fillContext = fillCanvas.getContext('2d'), + imageCanvas = document.createElement('canvas'), + imageContext = imageCanvas.getContext('2d'); + + imageCanvas.width = image.width; + imageCanvas.height = image.height; + + // Draw a 1px by 1px area of the canvas + fillContext.fillStyle = this.attrs.fill; + fillContext.fillRect(0, 0, 1, 1); + + var fill = fillContext.getImageData(0, 0, 1, 1).data; // Get Uint8ClampedArray of 1px by 1px area + + // Copy text sprite to new canvas + imageContext.drawImage(image, 0, 0, image.width, image.height); + + var imageData = imageContext.getImageData(0, 0, imageCanvas.width, imageCanvas.height), // Uint8ClampedArray of text sprite + data = imageData.data; + + // Loop through every pixel in text sprite and recolor + for (var p = 0; p < data.length; p += 4) { + // If not totally transparent + if (data[p + 3]) { + data[p] = fill[0]; // red + data[p + 1] = fill[1]; // green + data[p + 2] = fill[2]; // blue + } + } + + imageContext.putImageData(imageData, 0, 0); + + image = imageCanvas; + } + + for (; i < len; i++) { + var char = this.textData[i]; + context.drawImage(image, char.sx, char.sy, char.sw, char.sh, char.dx, char.dy, char.dw, char.dh); + } + }, + _hitFunc: function(context) { + + var i = 0, + len = this.lineWidths.length, + lineHeight = this.getLineHeight(); + + // Loop through each line and draw hit area for that line + for (; i < len; i++) { + context.beginPath(); + context.rect(0, lineHeight * i, this.lineWidths[i], lineHeight); + context.closePath(); + context.fillStrokeShape(this); + } + } + }; + + Kinetic.Factory.addGetterSetter(Kinetic.BitmapText, 'text'); + Kinetic.Factory.addGetterSetter(Kinetic.BitmapText, 'image'); + Kinetic.Factory.addGetterSetter(Kinetic.BitmapText, 'chars'); + Kinetic.Factory.addGetterSetter(Kinetic.BitmapText, 'padding'); + Kinetic.Factory.addGetterSetter(Kinetic.BitmapText, 'charSpacing'); + + Kinetic.Util.extend(Kinetic.BitmapText, Kinetic.Shape); + Kinetic.Collection.mapMethods(Kinetic.BitmapText); +})(); \ No newline at end of file diff --git a/test/unit/plugins/BitmapText-test.js b/test/unit/plugins/BitmapText-test.js new file mode 100644 index 00000000..daeeb9e3 --- /dev/null +++ b/test/unit/plugins/BitmapText-test.js @@ -0,0 +1,43 @@ +suite('BitmapText', function() { + + // ====================================================== + test('add bitmap text', function() { + + var imageObj = new Image(); + + imageObj.onload = function() { + + var stage = addStage(), + layer = new Kinetic.Layer(), + text = new Kinetic.BitmapText(); + + layer.add(text); + + assert.equal(text.getClassName(), 'BitmapText', 'getClassName should be BitmapText'); + } + + imageObj.src = 'assets/font.gif'; + }); + + test('bitmap text non-string values', function() { + + var imageObj = new Image(); + + imageObj.onload = function() { + + var stage = addStage(), + layer = new Kinetic.Layer(); + + var text = new Kinetic.BitmapText({ + text: NaN + }); + + layer.add(text); + + assert.equal(text.getClassName(), 'BitmapText', 'getClassName should be BitmapText'); + } + + imageObj.src = 'assets/font.gif'; + }); + +}); \ No newline at end of file From 7c6151d49ded1d18fd3bdda900757898bd182eba Mon Sep 17 00:00:00 2001 From: Victor Michnowicz Date: Thu, 6 Nov 2014 22:17:12 -0500 Subject: [PATCH 2/2] Update tests --- src/plugins/BitmapText.js | 53 +++++++++-------- test/assets/font.gif | Bin 0 -> 3353 bytes test/node-runner.js | 1 + test/runner.html | 1 + test/unit/plugins/BitmapText-test.js | 84 ++++++++++++++++++++++++++- 5 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 test/assets/font.gif diff --git a/src/plugins/BitmapText.js b/src/plugins/BitmapText.js index f8a51f71..33f67ecf 100644 --- a/src/plugins/BitmapText.js +++ b/src/plugins/BitmapText.js @@ -23,7 +23,6 @@ Kinetic.Shape.call(this, config); // This shape will act as a Kinetic group self.className = BITMAPTEXT; - self.attrs.cgaCache = {}; // Empty object for cache this._addListeners(); @@ -40,7 +39,7 @@ el = document.createElement('div'); // Empty string values - if (typeof text === 'undefined' || text === null || isNaN(text)) { + if (typeof text === 'undefined' || typeof text === 'boolean' || text === null || ( typeof text === 'number' && isNaN(text) ) ) { text = ''; } @@ -57,7 +56,7 @@ height = null, line = 0, lineHeight = this.getLineHeight(); - + for (; i < len; i++) { var node = nodes[i], tag = node.tagName ? node.tagName.toLowerCase() : STANDARD, @@ -75,11 +74,11 @@ // Loop through each character for (var c = 0; c < text.length; c++) { - var char = text.charAt(c), - pos = this._getPos(char, tag); + var character = text.charAt(c), + pos = this._getPos(character, tag); this.textData.push({ - char: char, + character: character, tag: tag, sx: pos.x, // Source X sy: pos.y, // Source Y @@ -125,17 +124,23 @@ var height = 0; + // If we have object of text characters if (typeof this.attrs.chars === 'object') { - if (typeof this.attrs.chars[tag]['1'] === 'object' ) { - height = this.attrs.chars[tag]['1'][3]; - } - - if (typeof this.attrs.chars[tag]['T'] === 'object' ) { - height = Math.max(height, this.attrs.chars[tag]['1'][3]); - } - - if (typeof this.attrs.chars[tag]['L'] === 'object' ) { - height = Math.max(this.attrs.chars[tag]['1'][3]); + + // If we have this character tag (default to standard) + if (typeof this.attrs.chars[tag] === 'object') { + + if (typeof this.attrs.chars[tag]['1'] === 'object' ) { + height = this.attrs.chars[tag]['1'][3]; + } + + if (typeof this.attrs.chars[tag]['T'] === 'object' ) { + height = Math.max(height, this.attrs.chars[tag]['1'][3]); + } + + if (typeof this.attrs.chars[tag]['L'] === 'object' ) { + height = Math.max(this.attrs.chars[tag]['1'][3]); + } } // If we could not determine a line height @@ -161,12 +166,12 @@ /** * Get position and dimensions of a character * - * @param char + * @param character * @param tag * @returns {{x: number, y: number, width: number, height: number}} * @private */ - _getPos: function(char, tag) { + _getPos: function(character, tag) { var temp = null, pos = { x: 0, @@ -177,12 +182,12 @@ if (typeof this.attrs.chars === 'object') { // If this tag exists and this character exists with this tag - if (this.attrs.chars[tag] && typeof this.attrs.chars[tag][char] === 'object') { - temp = this.attrs.chars[tag][char]; + if (this.attrs.chars[tag] && typeof this.attrs.chars[tag][character] === 'object') { + temp = this.attrs.chars[tag][character]; } // Else, if this character exists with a standard tag - else if (typeof this.attrs.chars[STANDARD][char] === 'object') { - temp = this.attrs.chars[STANDARD][char]; + else if (typeof this.attrs.chars[STANDARD][character] === 'object') { + temp = this.attrs.chars[STANDARD][character]; } } @@ -250,8 +255,8 @@ } for (; i < len; i++) { - var char = this.textData[i]; - context.drawImage(image, char.sx, char.sy, char.sw, char.sh, char.dx, char.dy, char.dw, char.dh); + var character = this.textData[i]; + context.drawImage(image, character.sx, character.sy, character.sw, character.sh, character.dx, character.dy, character.dw, character.dh); } }, _hitFunc: function(context) { diff --git a/test/assets/font.gif b/test/assets/font.gif new file mode 100644 index 0000000000000000000000000000000000000000..f384020124bce4c1005f92a853aae3922d86ab0b GIT binary patch literal 3353 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 literal 0 HcmV?d00001 diff --git a/test/node-runner.js b/test/node-runner.js index e3c9e9ad..bff09ee4 100644 --- a/test/node-runner.js +++ b/test/node-runner.js @@ -100,6 +100,7 @@ require('./unit/plugins/Star-test.js'); require('./unit/plugins/RegularPolygon-test.js'); require('./unit/plugins/Path-test.js'); require('./unit/plugins/TextPath-test.js'); +require('./unit/plugins/BitmapText-test.js'); // // filters --> require('./unit/filters/Blur-test.js'); diff --git a/test/runner.html b/test/runner.html index a3d277c3..f2c7987e 100644 --- a/test/runner.html +++ b/test/runner.html @@ -82,6 +82,7 @@

KineticJS Test

+ diff --git a/test/unit/plugins/BitmapText-test.js b/test/unit/plugins/BitmapText-test.js index daeeb9e3..7877fa68 100644 --- a/test/unit/plugins/BitmapText-test.js +++ b/test/unit/plugins/BitmapText-test.js @@ -19,6 +19,7 @@ suite('BitmapText', function() { imageObj.src = 'assets/font.gif'; }); + // ====================================================== test('bitmap text non-string values', function() { var imageObj = new Image(); @@ -28,13 +29,90 @@ suite('BitmapText', function() { var stage = addStage(), layer = new Kinetic.Layer(); - var text = new Kinetic.BitmapText({ + var text1 = new Kinetic.BitmapText({ text: NaN }); - layer.add(text); + var text2 = new Kinetic.BitmapText({ + text: null + }); + + var text3 = new Kinetic.BitmapText({ + text: undefined + }); + + var text4 = new Kinetic.BitmapText({ + text: false + }); + + var text5 = new Kinetic.BitmapText({ + text: true + }); + + layer.add(text1, text2, text3, text4, text5); + + assert.equal(text1.textData.length, 0, 'NaN evaluate to empty string'); + assert.equal(text2.textData.length, 0, 'null evaluate to empty string'); + assert.equal(text3.textData.length, 0, 'undefined evaluate to empty string'); + assert.equal(text4.textData.length, 0, 'false evaluate to empty string'); + assert.equal(text5.textData.length, 0, 'true evaluate to empty string'); + } + + imageObj.src = 'assets/font.gif'; + }); + + // ====================================================== + test('bitmap text line height', function() { + + var imageObj = new Image(); + + imageObj.onload = function() { + + var stage = addStage(), + layer = new Kinetic.Layer(), + text1 = new Kinetic.BitmapText(), + text2 = new Kinetic.BitmapText(), + text3 = new Kinetic.BitmapText(); + + text1.setAttrs({ + image: imageObj, + lineHeight: 12, + chars: { + standard: { + 'a': [0, 0, 10, 1], + '1': [0, 0, 10, 2], + 'T': [0, 0, 10, 3], + 'L': [0, 0, 10, 4] + } + } + }); + + text2.setAttrs({ + image: imageObj, + chars: { + standard: { + 'a': [0, 0, 10, 1], + '1': [0, 0, 10, 2], + 'T': [0, 0, 10, 3], + 'L': [0, 0, 10, 4] + } + } + }); + + text3.setAttrs({ + image: imageObj, + chars: { + standard: { + 'a': [0, 0, 10, 1] + } + } + }); + + layer.add(text1, text2, text3); - assert.equal(text.getClassName(), 'BitmapText', 'getClassName should be BitmapText'); + assert.equal(text1.getLineHeight(), 12, 'User defined line height should override default logic'); + assert.equal(text2.getLineHeight(), 2, 'Line height should default to height of "1" character'); + assert.equal(text3.getLineHeight(), 1, 'Line height should fall back to height of first defined character'); } imageObj.src = 'assets/font.gif';