diff --git a/docs/components/text.md b/docs/components/text.md
index 0efcef36d4b..4c50303ce77 100644
--- a/docs/components/text.md
+++ b/docs/components/text.md
@@ -22,7 +22,7 @@ The text component renders bitmap and signed distance field font text.
| letterSpacing | The letter spacing, in pixels. | 0 |
| lineHeight | The line height, in pixels. | undefined (from font) |
| opacity | Opacity (0 = fully transparent, 1 = fully opaque) | 1.0 |
-| shader | Shader to render text. (modified-sdf, sdf, basic, msdf) | modified-sdf |
+| shader | Shader to render text. (modifiedsdf, sdf, basic, msdf) | modifiedsdf |
| side | Side to render. (front, back, double) | front |
| tabSize | Tab size, in spaces. | 4 |
| transparent | Should text be transparent? | true |
@@ -31,6 +31,7 @@ The text component renders bitmap and signed distance field font text.
| width | Width (default = geometry width, DEFAULT_WIDTH if none) | |
| wrapCount | Wrap after this many font characters (more or less). | 40 |
| wrapPixels | Wrap after this many pixels. | undefined (wrapCount) |
+| zOffset | Z offset to apply to avoid Z-fighting. | 0.001 |
More details on these properties [here](https://github.com/Jam3/three-bmfont-text#usage).
diff --git a/docs/primitives/a-text.md b/docs/primitives/a-text.md
new file mode 100644
index 00000000000..4da06c95de0
--- /dev/null
+++ b/docs/primitives/a-text.md
@@ -0,0 +1,80 @@
+---
+parent_section: primitives
+---
+
+[text]: ../components/text.md
+The a-text primitive renders bitmap and signed distance field font text using the [text][text] component.
+
+## Properties
+
+| Property | Component Map | Description | Default Value |
+|:--------------:|:------------------:|:-------------------------------------------------------:|:---------------------:|
+| align | text.align | Multi-line text alignment (left, center, right). | left |
+| alpha-test | text.alphaTest | Discard text pixels if alpha is less than this. | 0.5 |
+| anchor | text.anchor | Horizontal positioning (left, center, right, align). | center |
+| baseline | text.baseline | Vertical positioning (top, center, bottom). | center |
+| color | text.color | Text color. | #000 (which is black) |
+| font | text.font | Font file to render text. (stock names; or .fnt URL) | default |
+| font-image | text.fontImage | Font image to render text. (from font, or override) | undefined (from font) |
+| height | text.height | Height of text block. | undefined (from text) |
+| letter-spacing | text.letterSpacing | The letter spacing, in pixels. | 0 |
+| line-height | text.lineHeight | The line height, in pixels. | undefined (from font) |
+| opacity | text.opacity | Opacity (0 = fully transparent, 1 = fully opaque) | 1.0 |
+| shader | text.shader | Shader to render text. (modifiedsdf, sdf, basic, msdf) | modifiedsdf |
+| side | text.side | Side to render. (front, back, double) | front |
+| tabSize | text.tabSize | Tab size, in spaces. | 4 |
+| transparent | text.transparent | Should text be transparent? | true |
+| value | text.value | The text to render. | |
+| whitespace | text.whitespace | How should whitespace be handled? (normal, pre, nowrap) | normal |
+| width | text.width | Width (default = geometry width, DEFAULT_WIDTH if none) | |
+| wrap-count | text.wrapCount | Wrap after this many font characters (more or less). | 40 |
+| wrap-pixels | text.wrapPixels | Wrap after this many pixels. | undefined (wrapCount) |
+| z-offset | text.zOffset | Z offset to apply to avoid Z-fighting. | 0.001 |
+
+More details on these properties [here](https://github.com/Jam3/three-bmfont-text#usage).
+
+Explanation of whitespace (formerly 'mode') property [here](https://github.com/mattdesl/word-wrapper).
+
+## Usage
+
+Write some text:
+
+```html
+
+```
+
+To change the size of the text, increase width, decrease wrapCount (roughly how many characters to fit inside the given width) or wrapPixels,
+use the [scale](https://aframe.io/docs/master/components/scale.html) component or position the text closer or further away.
+
+Text can be wrapped by specifying width in A-Frame units.
+
+## Custom Fonts
+
+A guide for generating SDF fonts can be found [here](https://github.com/libgdx/libgdx/wiki/Distance-field-fonts);
+here is an example comparing [Arial Black and DejaVu](http://i.imgur.com/iWtXHm5.png).
+Bitmap fonts also work, but do not look nearly as good.
+
+Different fonts can be specified with the 'font' and 'fontImage' properties.
+
+```html
+
+
+```
+
+## Limitations
+
+This component does not make use of all of the features of [three-bmfont-text](https://github.com/Jam3/three-bmfont-text) and its sister modules.
+
+Bitmap font rendering limits you to the characters included in the font (Unicode this is not).
+SDF font (in particular) tends to smooth sharp edges though [there are ways around this](https://lambdacube3d.wordpress.com/2014/11/12/playing-around-with-font-rendering/).
+
+#### Additional Information
+
+If you are interested in text rendering in WebGL/ThreeJS/A-Frame and want to learn more,
+reading the documentation for [three-bmfont-text](https://github.com/Jam3/three-bmfont-text) is recommended.
+
+Here are some additional resources:
+
+- ['It’s 2015 and drawing text is still hard (WebGL, ThreeJS)' by Parris Khachi](https://www.eventbrite.com/engineering/its-2015-and-drawing-text-is-still-hard-webgl-threejs/)
+- [Valve's original paper](http://www.valvesoftware.com/publications/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf)
+- ['Hacking with THREE.js' by Matt DesLauriers](http://slides.com/mattdeslauriers/hacking-with-three-js#/13)
diff --git a/examples/index.html b/examples/index.html
index d05f660f698..af9c3eb3451 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -228,9 +228,9 @@
Tests
Raycaster
Shaders
Text
- Text (Fonts)
- Text (Sizes)
- Text (Scenarios)
+ Text Anchors
+ Text Scenarios
+ Text Sizes
Towers
Video
Video 360°
diff --git a/examples/test/text/anchor.html b/examples/test/text/anchor.html
deleted file mode 100644
index e663cdb7307..00000000000
--- a/examples/test/text/anchor.html
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
- A-Frame BMFont Text Component - Anchor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/test/text/anchors.html b/examples/test/text/anchors.html
new file mode 100644
index 00000000000..57c237b03bd
--- /dev/null
+++ b/examples/test/text/anchors.html
@@ -0,0 +1,87 @@
+
+
+ Text Anchors
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/test/text/index.html b/examples/test/text/index.html
index b4b4710df91..3d537d813a2 100644
--- a/examples/test/text/index.html
+++ b/examples/test/text/index.html
@@ -1,70 +1,83 @@
- Example
+ Text
+
-
-
-
+
+
+
-
+
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
+
-
+
-
+
-
+
-
+
+
+
+
+
-
diff --git a/examples/test/text/scenarios.html b/examples/test/text/scenarios.html
index d9c4b6b045c..a6df99365a2 100644
--- a/examples/test/text/scenarios.html
+++ b/examples/test/text/scenarios.html
@@ -1,103 +1,102 @@
- Scenarios
+ Text Scenarios
+
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+ -->
+
-
-
+ -->
+
-
-
+ -->
+
-
-
-
+ -->
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/examples/test/text/sizes.html b/examples/test/text/sizes.html
index 758beeddb99..40bf7212586 100644
--- a/examples/test/text/sizes.html
+++ b/examples/test/text/sizes.html
@@ -1,45 +1,38 @@
- Example
+ Text Sizes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/text.js b/src/components/text.js
index 77692b90d3a..6ec161ffb5b 100644
--- a/src/components/text.js
+++ b/src/components/text.js
@@ -10,6 +10,7 @@ var coreShader = require('../core/shader');
var THREE = require('../lib/three');
var utils = require('../utils/');
+var error = utils.debug('components:text:error');
var shaders = coreShader.shaders;
var warn = utils.debug('components:text:warn');
@@ -31,6 +32,7 @@ var FONTS = {
mozillavr: FONT_BASE_URL + 'mozillavr.fnt',
sourcecodepro: FONT_BASE_URL + 'SourceCodePro.fnt'
};
+module.exports.FONTS = FONTS;
var cache = new PromiseCache();
var fontWidthFactors = {};
@@ -74,7 +76,9 @@ module.exports.Component = registerComponent('text', {
// `wrapCount` units are about one default font character. Wrap roughly at this number.
wrapCount: {type: 'number', default: 40},
// `wrapPixels` will wrap using bmfont pixel units (e.g., dejavu's is 32 pixels).
- wrapPixels: {type: 'number'}
+ wrapPixels: {type: 'number'},
+ // `zOffset` will provide a small z offset to avoid z-fighting
+ zOffset: {type: 'number', default: 0.001}
},
init: function () {
@@ -85,7 +89,7 @@ module.exports.Component = registerComponent('text', {
this.createOrUpdateMaterial();
this.mesh = new THREE.Mesh(this.geometry, this.material);
- this.el.setObject3D('text', this.mesh);
+ this.el.setObject3D(this.attrName, this.mesh);
},
update: function (oldData) {
@@ -105,12 +109,7 @@ module.exports.Component = registerComponent('text', {
// Update geometry and layout.
if (font) {
- this.geometry.update(utils.extend({}, data, {
- font: font,
- lineHeight: data.lineHeight || font.common.lineHeight,
- text: data.value,
- width: data.wrapPixels || ((0.5 + data.wrapCount) * font.widthFactor)
- }));
+ updateGeometry(this.geometry, data, font);
this.updateLayout(data);
}
},
@@ -121,7 +120,7 @@ module.exports.Component = registerComponent('text', {
remove: function () {
this.geometry.dispose();
this.geometry = null;
- this.el.removeObject3D('text');
+ this.el.removeObject3D(this.attrName);
this.material.dispose();
this.material = null;
this.texture.dispose();
@@ -192,21 +191,23 @@ module.exports.Component = registerComponent('text', {
},
/**
- * Fetch and apply font.
+ * Load font for geometry, load font image for material, and apply.
*/
updateFont: function () {
+ var el = this.el;
var fontSrc;
var geometry = this.geometry;
var self = this;
- if (!this.data.font) {
- warn('No font specified for `text`. Using the default font.');
- }
+ if (!this.data.font) { warn('No font specified. Using the default font.'); }
+
+ // Make invisible during font swap.
this.mesh.visible = false;
// Look up font URL to use, and perform cached load.
- fontSrc = lookupFont(this.data.font || 'default');
+ fontSrc = this.lookupFont(this.data.font || 'default');
cache.get(fontSrc, function () { return loadFont(fontSrc); }).then(function (font) {
+ var data;
var fontImgSrc;
if (font.pages.length !== 1) {
@@ -214,38 +215,16 @@ module.exports.Component = registerComponent('text', {
}
if (!fontWidthFactors[fontSrc]) {
- // Compute default font width factor to use.
- var sum = 0;
- var digitsum = 0;
- var digits = 0;
- font.chars.map(function (ch) {
- sum += ch.xadvance;
- if (ch.id >= 48 && ch.id <= 57) {
- digits++;
- digitsum += ch.xadvance;
- }
- });
- font.widthFactor = fontWidthFactors[font] = digits ? digitsum / digits : sum / font.chars.length;
+ font.widthFactor = fontWidthFactors[font] = computeFontWidthFactor(font);
}
// Update geometry given font metrics.
- var data = coerceData(self.data);
- var textRenderWidth = data.wrapPixels || ((0.5 + data.wrapCount) * font.widthFactor);
- var options = utils.extend({}, data, {
- text: data.value,
- font: font,
- width: textRenderWidth,
- lineHeight: data.lineHeight || font.common.lineHeight
- });
- var object3D;
- geometry.update(options);
- self.mesh.geometry = geometry;
-
- // Add mesh if not already there.
- object3D = self.el.object3D;
- if (object3D.children.indexOf(self.mesh) === -1) {
- object3D.add(self.mesh);
- }
+ data = coerceData(self.data);
+ updateGeometry(geometry, data, font);
+
+ // Set font and update layout.
+ self.currentFont = font;
+ self.updateLayout(data);
// Look up font image URL to use, and perform cached load.
fontImgSrc = self.data.fontImage || fontSrc.replace('.fnt', '.png') ||
@@ -255,83 +234,96 @@ module.exports.Component = registerComponent('text', {
}).then(function (image) {
// Make mesh visible and apply font image as texture.
self.mesh.visible = true;
- if (!image) { return; }
self.texture.image = image;
self.texture.needsUpdate = true;
- }).catch(function () {
- console.error('Could not load font texture "' + fontImgSrc +
- '"\nMake sure it is correctly defined in the bitmap .fnt file.');
+ el.emit('textfontset', {font: self.data.font, fontObj: font});
+ }).catch(function (err) {
+ error(err);
+ throw err;
});
-
- self.currentFont = font;
- self.updateLayout(data);
- }).catch(function (error) {
- throw new Error('Error loading font ' + self.data.font +
- '\nMake sure the path is correct and that it points' +
- ' to a valid BMFont file (xml, json, fnt).\n' + error.message);
+ }).catch(function (err) {
+ error(err);
+ throw err;
});
},
+ /**
+ * Update layout with anchor, alignment, baseline, and considering any meshes.
+ */
updateLayout: function (data) {
+ var anchor;
+ var baseline;
var el = this.el;
- var font = this.currentFont;
var geometry = this.geometry;
+ var geometryComponent;
+ var height;
var layout = geometry.layout;
- var elGeo = el.getAttribute('geometry');
- var width;
+ var mesh = this.mesh;
var textRenderWidth;
var textScale;
- var height;
+ var width;
var x;
var y;
- var anchor;
- var baseline;
- // Determine width to use.
- width = data.width || (elGeo && elGeo.width) || DEFAULT_WIDTH;
- // Determine wrap pixel count, either as specified or by experimentally determined fudge factor.
- // (Note that experimentally determined factor will never be correct for variable width fonts.)
- textRenderWidth = data.wrapPixels || ((0.5 + data.wrapCount) * font.widthFactor);
+ // Determine width to use (defined width, geometry's width, or default width).
+ width = data.width || (geometryComponent && geometryComponent.width) || DEFAULT_WIDTH;
+
+ // Determine wrap pixel count. Either specified or by experimental fudge factor.
+ // Note that experimental factor will never be correct for variable width fonts.
+ textRenderWidth = computeWidth(data.wrapPixels, data.wrapCount,
+ this.currentFont.widthFactor);
textScale = width / textRenderWidth;
- // Determine height to use.
- height = textScale * (geometry.layout.height + geometry.layout.descender);
- // update geometry dimensions to match layout, if not specified
- if (elGeo) {
- if (!elGeo.width) { el.setAttribute('geometry', 'width', width); }
- if (!elGeo.height) { el.setAttribute('geometry', 'height', height); }
+ // Determine height to use.
+ height = textScale * (layout.height + layout.descender);
+
+ // Update geometry dimensions to match text layout if width and height are set to 0.
+ // For example, scales a plane to fit text.
+ geometryComponent = el.getAttribute('geometry');
+ if (geometryComponent) {
+ if (!geometryComponent.width) { el.setAttribute('geometry', 'width', width); }
+ if (!geometryComponent.height) { el.setAttribute('geometry', 'height', height); }
}
- // anchors text left/center/right
+ // Calculate X position to anchor text left, center, or right.
anchor = data.anchor === 'align' ? data.align : data.anchor;
if (anchor === 'left') {
x = 0;
} else if (anchor === 'right') {
- x = -layout.width;
+ x = -1 * layout.width;
} else if (anchor === 'center') {
- x = -layout.width / 2;
+ x = -1 * layout.width / 2;
} else {
- throw new TypeError('invalid anchor ' + anchor);
+ throw new TypeError('Invalid text.anchor property value', anchor);
}
- // anchors text to top/center/bottom
+ // Calculate Y position to anchor text top, center, or bottom.
baseline = data.baseline;
if (baseline === 'bottom') {
y = 0;
} else if (baseline === 'top') {
- y = -layout.height + layout.ascender;
+ y = -1 * layout.height + layout.ascender;
} else if (baseline === 'center') {
- y = -layout.height / 2;
+ y = -1 * layout.height / 2;
} else {
- throw new TypeError('invalid baseline ' + baseline);
+ throw new TypeError('Invalid text.baseline property value', baseline);
}
- // Position and scale mesh.
- this.mesh.position.x = x * textScale;
- this.mesh.position.y = y * textScale;
- this.mesh.position.z = 0.001; // put text slightly in front in case there is a plane or other geometry
- this.mesh.scale.set(textScale, -textScale, textScale);
+ // Position and scale mesh to apply layout.
+ mesh.position.x = x * textScale;
+ mesh.position.y = y * textScale;
+ // Place text slightly in front to avoid Z-fighting.
+ mesh.position.z = data.zOffset;
+ mesh.scale.set(textScale, -1 * textScale, textScale);
this.geometry.computeBoundingSphere();
+ },
+
+ /**
+ * Grab font from the constant.
+ * Set as a method for test stubbing purposes.
+ */
+ lookupFont: function (key) {
+ return FONTS[key];
}
});
@@ -345,10 +337,6 @@ function unregisterFont (key) {
}
module.exports.unregisterFont = unregisterFont;
-function lookupFont (keyOrUrl) {
- return FONTS[keyOrUrl] || keyOrUrl;
-}
-
function parseSide (side) {
switch (side) {
case 'back': {
@@ -363,9 +351,11 @@ function parseSide (side) {
}
}
+/**
+ * Coerce some data to numbers.
+ * as they will be passed directly into text creation and update
+ */
function coerceData (data) {
- // We have to coerce some data to numbers/booleans,
- // as they will be passed directly into text creation and update
data = utils.clone(data);
if (data.lineHeight !== undefined) {
data.lineHeight = parseFloat(data.lineHeight);
@@ -385,6 +375,7 @@ function loadFont (src) {
return new Promise(function (resolve, reject) {
loadBMFont(src, function (err, font) {
if (err) {
+ error('Error loading font', src);
reject(err);
return;
}
@@ -401,6 +392,7 @@ function loadTexture (src) {
new THREE.ImageLoader().load(src, function (image) {
resolve(image);
}, undefined, function () {
+ error('Error loading font image', src);
reject(null);
});
});
@@ -434,6 +426,43 @@ function updateBaseMaterial (material, data) {
material.side = data.side;
}
+/**
+ * Update the text geometry using `three-bmfont-text.update`.
+ */
+function updateGeometry (geometry, data, font) {
+ geometry.update(utils.extend({}, data, {
+ font: font,
+ width: computeWidth(data.wrapPixels, data.wrapCount, font.widthFactor),
+ text: data.value.replace(/\\n/g, '\n'),
+ lineHeight: data.lineHeight || font.common.lineHeight
+ }));
+}
+
+/**
+ * Determine wrap pixel count. Either specified or by experimental fudge factor.
+ * Note that experimental factor will never be correct for variable width fonts.
+ */
+function computeWidth (wrapPixels, wrapCount, widthFactor) {
+ return wrapPixels || ((0.5 + wrapCount) * widthFactor);
+}
+
+/**
+ * Compute default font width factor to use.
+ */
+function computeFontWidthFactor (font) {
+ var sum = 0;
+ var digitsum = 0;
+ var digits = 0;
+ font.chars.map(function (ch) {
+ sum += ch.xadvance;
+ if (ch.id >= 48 && ch.id <= 57) {
+ digits++;
+ digitsum += ch.xadvance;
+ }
+ });
+ return digits ? digitsum / digits : sum / font.chars.length;
+}
+
/**
* Get or create a promise given a key and promise generator.
* @todo Move to a utility and use in other parts of A-Frame.
diff --git a/src/extras/primitives/primitives.js b/src/extras/primitives/primitives.js
index 034580bd476..048cc6d30df 100644
--- a/src/extras/primitives/primitives.js
+++ b/src/extras/primitives/primitives.js
@@ -127,3 +127,30 @@ module.exports.registerPrimitive = function registerPrimitive (name, definition)
primitives[name] = primitive;
return primitive;
};
+
+function addComponentMapping (srcComponent, dstMap) {
+ // FIXME: need to get from component name to schema
+ var srcComponentSchema = components[srcComponent].schema;
+ Object.keys(srcComponentSchema).map(function (prop) {
+ // Hyphenate where there is camelCase.
+ var htmlAttrName = prop.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ // If there is a mapping collision, prefix with component name and hyphen.
+ if (dstMap[htmlAttrName] !== undefined) { htmlAttrName = srcComponent + '-' + prop; }
+ dstMap[htmlAttrName] = srcComponent + '.' + prop;
+ });
+}
+
+module.exports.definePrimitive = function definePrimitive (primitiveName, defaultComponents, /* optional */ mappings) {
+ // If no initial mappings provided, start from empty map.
+ mappings = mappings || {};
+
+ // From the default components, add mapping automagically.
+ Object.keys(defaultComponents).map(function (componentName) { addComponentMapping(componentName, mappings); });
+
+ // Register the primitive.
+ module.exports.registerPrimitive(primitiveName, utils.extendDeep({}, null, {
+ defaultComponents: defaultComponents,
+ mappings: mappings
+ }));
+};
+
diff --git a/src/extras/primitives/primitives/a-text.js b/src/extras/primitives/primitives/a-text.js
index cd62b5f7a55..2b61bd40172 100644
--- a/src/extras/primitives/primitives/a-text.js
+++ b/src/extras/primitives/primitives/a-text.js
@@ -1,28 +1,9 @@
/* Experimental text primitive.
*/
-var getMeshMixin = require('../getMeshMixin');
-var registerPrimitive = require('../primitives').registerPrimitive;
-var utils = require('../../../utils/');
+var definePrimitive = require('../primitives').definePrimitive;
-registerPrimitive('a-text', utils.extendDeep({}, getMeshMixin(), {
- defaultComponents: {
- 'text': {anchor: 'align', width: 5}
- },
- mappings: {
- align: 'text.align',
- anchor: 'text.anchor',
- baseline: 'text.baseline',
- color: 'text.color',
- height: 'text.height',
- letterspacing: 'text.letterSpacing',
- lineheight: 'text.lineHeight',
- font: 'text.font',
- fontimage: 'text.fontImage',
- mode: 'text.mode',
- opacity: 'text.opacity',
- value: 'text.value',
- width: 'text.width',
- wrapcount: 'text.wrapCount',
- wrappixels: 'text.wrapPixels'
- }
-}));
+definePrimitive('a-text',
+ // default component(s) and defaults
+ { text: { anchor: 'align', width: 5 } }
+ // no other mappings
+);
diff --git a/src/shaders/modifiedsdf.js b/src/shaders/modifiedsdf.js
index 505b0354231..afae7d401f6 100644
--- a/src/shaders/modifiedsdf.js
+++ b/src/shaders/modifiedsdf.js
@@ -20,26 +20,79 @@ module.exports.Shader = registerShader('modifiedsdf', {
].join('\n'),
fragmentShader: [
- '#define ALL_SMOOTH 0.5',
- '#define ALL_ROUGH 0.4',
- '#define DISCARD_ALPHA 0.1',
+ '#ifdef GL_OES_standard_derivatives',
+ '#extension GL_OES_standard_derivatives: enable',
+ '#endif',
+ // FIXME: experimentally determined constants
+ '#define BIG_ENOUGH 0.001',
+ '#define MODIFIED_ALPHATEST (0.02 * isBigEnough / BIG_ENOUGH)',
+ '#define ALL_SMOOTH 0.4',
+ '#define ALL_ROUGH 0.02',
+ '#define DISCARD_ALPHA (alphaTest / (2.2 - 1.2 * ratio))',
'uniform sampler2D map;',
'uniform vec3 color;',
'uniform float opacity;',
'uniform float alphaTest;',
'varying vec2 vUV;',
- 'float aastep(float value) {',
- ' float afwidth = (1.0 / 32.0) * (1.4142135623730951 / (2.0 * gl_FragCoord.w));',
+ '#ifdef GL_OES_standard_derivatives',
+ 'float contour(float width, float value) {',
+ ' return smoothstep(0.5 - value, 0.5 + value, width);',
+ '}',
+ '#else',
+ 'float aastep(float value, float afwidth) {',
' return smoothstep(0.5 - afwidth, 0.5 + afwidth, value);',
'}',
+ '#endif',
'void main() {',
+ '#ifdef GL_OES_standard_derivatives',
+ // when we have derivatives and can get texel size etc., that allows supersampling etc.
+ ' vec2 uv = vUV;',
+ ' vec4 texColor = texture2D(map, uv);',
+ ' float dist = texColor.a;',
+ ' float width = fwidth(dist);',
+ ' float alpha = contour(dist, width);',
+ ' float dscale = 0.353505;',
+ ' vec2 duv = dscale * (dFdx(uv) + dFdy(uv));',
+ ' float isBigEnough = max(abs(duv.x), abs(duv.y));',
+ // when texel is too small, blend raw alpha value rather than supersampling etc.
+ // FIXME: experimentally determined constant
+ ' if (isBigEnough > BIG_ENOUGH) {',
+ ' float ratio = BIG_ENOUGH / isBigEnough;',
+ ' alpha = ratio * alpha + (1.0 - ratio) * dist;',
+ ' }',
+ // otherwise do weighted supersampling
+ // FIXME: why this weighting?
+ ' else if (isBigEnough <= BIG_ENOUGH) {',
+ ' vec4 box = vec4 (uv - duv, uv + duv);',
+ ' alpha = (alpha + 0.5 * (',
+ ' contour(texture2D(map, box.xy).a, width)',
+ ' + contour(texture2D(map, box.zw).a, width)',
+ ' + contour(texture2D(map, box.xw).a, width)',
+ ' + contour(texture2D(map, box.zy).a, width)',
+ ' )) / 3.0;',
+ ' }',
+ // when texel is big enough, do standard alpha test
+ // FIXME: experimentally determined constant
+ // looks much better if we DON'T do this, but do we get Z fighting etc.?
+ ' if (isBigEnough <= BIG_ENOUGH && alpha < alphaTest) { discard; return; }',
+ // else do modified alpha test
+ // FIXME: experimentally determined constant
+ ' if (alpha < alphaTest * MODIFIED_ALPHATEST) { discard; return; }',
+ '#else',
' vec4 texColor = texture2D(map, vUV);',
' float value = texColor.a;',
- ' float alpha = aastep(value);',
+ // when we don't have derivatives, use approximations
+ // FIXME: if we understood font pixel dimensions, this could probably be improved
+ ' float afwidth = (1.0 / 32.0) * (1.4142135623730951 / (2.0 * gl_FragCoord.w));',
+ ' float alpha = aastep(value, afwidth);',
+ // use gl_FragCoord.w to guess when we should blend
+ // FIXME: if we understood font pixel dimensions, this could probably be improved
' float ratio = (gl_FragCoord.w >= ALL_SMOOTH) ? 1.0 : (gl_FragCoord.w < ALL_ROUGH) ? 0.0 : (gl_FragCoord.w - ALL_ROUGH) / (ALL_SMOOTH - ALL_ROUGH);',
' if (alpha < alphaTest) { if (ratio >= 1.0) { discard; return; } alpha = 0.0; }',
' alpha = alpha * ratio + (1.0 - ratio) * value;',
- ' if (ratio < 1.0 && alpha <= DISCARD_ALPHA) { discard; return; }',
+ ' if (ratio < 1.0)',
+ ' if (alpha <= DISCARD_ALPHA) { discard; return; }',
+ '#endif',
' gl_FragColor = vec4(color, opacity * alpha);',
'}'
].join('\n')
diff --git a/tests/components/text.test.js b/tests/components/text.test.js
index 17ef082d0d2..7433e504f83 100644
--- a/tests/components/text.test.js
+++ b/tests/components/text.test.js
@@ -1,11 +1,19 @@
/* global assert, setup, suite, test, THREE */
+var Component = require('components/text').Component;
var entityFactory = require('../helpers').entityFactory;
-suite.only('text', function () {
+suite('text', function () {
var component;
var el;
setup(function (done) {
+ this.sinon.stub(Component.prototype, 'lookupFont', function (key) {
+ return {
+ default: '/base/tests/assets/test.fnt',
+ mozillavr: '/base/tests/assets/test.fnt?foo'
+ }[key];
+ });
+
el = entityFactory();
el.addEventListener('componentinitialized', function (evt) {
if (evt.detail.name !== 'text') { return; }
@@ -25,10 +33,51 @@ suite.only('text', function () {
});
suite('update', function () {
- test('updates geometry with value', function () {
+ test('updates geometry with value', function (done) {
+ // There are two paths by which geometry update can happen:
+ // 1. As after-effect of font change.
+ // 2. As direct effect when no font change.
var updateGeometrySpy = this.sinon.spy(component.geometry, 'update');
el.setAttribute('text', 'value', 'foo');
- assert.equal(updateGeometrySpy.getCalls()[0].args[0].value, 'foo');
+ if (component.currentFont) {
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].value, 'foo');
+ done();
+ } else {
+ el.addEventListener('textfontset', evt => {
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].value, 'foo');
+ done();
+ });
+ }
+ });
+
+ test('updates geometry with align', function () {
+ var updateGeometrySpy = this.sinon.spy(component.geometry, 'update');
+ el.setAttribute('text', 'align', 'right');
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].align, 'right');
+ });
+
+ test('updates geometry with letterSpacing', function () {
+ var updateGeometrySpy = this.sinon.spy(component.geometry, 'update');
+ el.setAttribute('text', 'letterSpacing', 2);
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].letterSpacing, 2);
+ });
+
+ test('updates geometry with lineHeight', function () {
+ var updateGeometrySpy = this.sinon.spy(component.geometry, 'update');
+ el.setAttribute('text', 'lineHeight', 2);
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].lineHeight, 2);
+ });
+
+ test('updates geometry with tabSize', function () {
+ var updateGeometrySpy = this.sinon.spy(component.geometry, 'update');
+ el.setAttribute('text', 'tabSize', 2);
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].tabSize, 2);
+ });
+
+ test('updates geometry with whiteSpace', function () {
+ var updateGeometrySpy = this.sinon.spy(component.geometry, 'update');
+ el.setAttribute('text', 'whiteSpace', 'nowrap');
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].whiteSpace, 'nowrap');
});
test('calls createOrUpdateMaterial if shader changes', function () {
@@ -106,6 +155,91 @@ suite.only('text', function () {
});
});
+ suite('updateFont', function () {
+ test('loads font', function (done) {
+ el.addEventListener('textfontset', evt => {
+ assert.equal(evt.detail.font, 'mozillavr');
+ assert.equal(component.texture.image.getAttribute('src'),
+ '/base/tests/assets/test.png?foo');
+ assert.ok(el.getObject3D('text').visible);
+ done();
+ });
+ el.setAttribute('text', 'font', 'mozillavr');
+ });
+
+ test('updates geometry', function (done) {
+ var updateGeometrySpy = this.sinon.spy(component.geometry, 'update');
+
+ el.addEventListener('textfontset', evt => {
+ assert.equal(updateGeometrySpy.getCalls()[0].args[0].font, evt.detail.fontObj);
+ done();
+ });
+ el.setAttribute('text', 'font', 'mozillavr');
+ });
+
+ test('loads font with specified font image', function (done) {
+ el.addEventListener('textfontset', evt => {
+ assert.equal(evt.detail.font, 'mozillavr');
+ assert.equal(component.texture.image.getAttribute('src'),
+ '/base/tests/assets/test2.png');
+ done();
+ });
+ el.setAttribute('text', {font: 'mozillavr', fontImage: '/base/tests/assets/test2.png'});
+ });
+ });
+
+ suite('updateLayout', function () {
+ test('anchors left', function () {
+ el.setAttribute('text', {anchor: 'left', value: 'a'});
+ assert.equal(el.getObject3D('text').position.x, 0);
+ });
+
+ test('anchors right', function () {
+ el.setAttribute('text', {anchor: 'right', value: 'a'});
+ assert.equal(el.getObject3D('text').position.x, -1);
+ });
+
+ test('anchors center', function () {
+ el.setAttribute('text', {anchor: 'center', value: 'a'});
+ assert.equal(el.getObject3D('text').position.x, -0.5);
+ });
+
+ test('baselines bottom', function () {
+ el.setAttribute('text', {baseline: 'bottom', value: 'a'});
+ assert.equal(el.getObject3D('text').position.y, 0);
+ });
+
+ test('baselines top and center', function () {
+ var yTop;
+ var yCenter;
+ el.setAttribute('text', {baseline: 'top', value: 'a'});
+ yTop = el.getObject3D('text').position.y;
+ el.setAttribute('text', {baseline: 'center', value: 'a'});
+ yCenter = el.getObject3D('text').position.y;
+ assert.ok(yTop < yCenter);
+ });
+
+ test('avoids z-fighting', function () {
+ assert.ok(el.getObject3D('text').position.z);
+ });
+
+ test('sets text scale', function () {
+ assert.notEqual(el.getObject3D('text').scale.x, 1);
+ assert.notEqual(el.getObject3D('text').scale.y, 1);
+ assert.notEqual(el.getObject3D('text').scale.z, 1);
+ });
+
+ test('autoscales mesh', function () {
+ el.setAttribute('geometry', {primitive: 'plane', height: 0, width: 0});
+ assert.equal(el.getAttribute('geometry').width, 0);
+ assert.equal(el.getAttribute('geometry').height, 0);
+
+ el.setAttribute('text', {width: 10, value: 'a'});
+ assert.equal(el.getAttribute('geometry').width, 10);
+ assert.ok(el.getAttribute('geometry').height);
+ });
+ });
+
suite('remove', function () {
test('removes mesh', function () {
el.parentNode.removeChild(el);