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);