UNPKG

three-mesh-ui

Version:

a library on top of three.js to help in creating 3D user interfaces

440 lines (274 loc) 11.3 kB
/** Job: Positioning inline elements according to their dimensions inside this component Knows: This component dimensions, and its children dimensions This module is used for Block composition (Object.assign). A Block is responsible for the positioning of its inline elements. In order for it to know what is the size of these inline components, parseParams must be called on its children first. It's worth noting that a Text is not positioned as a whole, but letter per letter, in order to create a line break when necessary. It's Text that merge the various letters in its own updateLayout function. */ import * as Whitespace from '../../utils/inline-layout/Whitespace'; import * as TextAlign from '../../utils/inline-layout/TextAlign'; export default function InlineManager( Base ) { return class InlineManager extends Base { /** Compute children .inlines objects position, according to their pre-computed dimensions */ computeInlinesPosition() { // computed by BoxComponent const INNER_WIDTH = this.getWidth() - ( this.padding * 2 || 0 ); const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); // got by MeshUIComponent const JUSTIFICATION = this.getJustifyContent(); const ALIGNMENT = this.getTextAlign(); const INTERLINE = this.getInterLine(); // Compute lines const lines = this.computeLines(); lines.interLine = INTERLINE; ///////////////////////////////////////////////////////////////// // Position lines according to justifyContent and contentAlign ///////////////////////////////////////////////////////////////// const textHeight = Math.abs( lines.height ); // Line vertical positioning const justificationOffset = ( () => { switch ( JUSTIFICATION ) { case 'start': return (INNER_HEIGHT/2); case 'end': return textHeight - ( INNER_HEIGHT / 2 ); case 'center': return ( textHeight / 2 ); default: console.warn( `justifyContent: '${JUSTIFICATION}' is not valid` ); } } )(); // lines.forEach( ( line ) => { line.y += justificationOffset; line.forEach( ( inline ) => { inline.offsetY += justificationOffset; } ); } ); // Horizontal positioning TextAlign.textAlign( lines, ALIGNMENT, INNER_WIDTH ); // Make lines accessible to provide helpful informations this.lines = lines; } calculateBestFit( bestFit ) { if ( this.childrenInlines.length === 0 ) return; switch ( bestFit ) { case 'grow': this.calculateGrowFit(); break; case 'shrink': this.calculateShrinkFit(); break; case 'auto': this.calculateAutoFit(); break; } } calculateGrowFit() { const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); //Iterative method to find a fontSize of text children that text will fit into container let iterations = 1; const heightTolerance = 0.075; const firstText = this.childrenInlines.find( inlineComponent => inlineComponent.isText ); let minFontMultiplier = 1; let maxFontMultiplier = 2; let fontMultiplier = firstText._fitFontSize ? firstText._fitFontSize / firstText.getFontSize() : 1; let textHeight; do { textHeight = this.calculateHeight( fontMultiplier ); if ( textHeight > INNER_HEIGHT ) { if ( fontMultiplier <= minFontMultiplier ) { // can't shrink text this.childrenInlines.forEach( inlineComponent => { if ( inlineComponent.isInlineBlock ) return; // ensure fontSize does not shrink inlineComponent._fitFontSize = inlineComponent.getFontSize(); } ); break; } maxFontMultiplier = fontMultiplier; fontMultiplier -= ( maxFontMultiplier - minFontMultiplier ) / 2; } else { if ( Math.abs( INNER_HEIGHT - textHeight ) < heightTolerance ) break; if ( Math.abs( fontMultiplier - maxFontMultiplier ) < 5e-10 ) maxFontMultiplier *= 2; minFontMultiplier = fontMultiplier; fontMultiplier += ( maxFontMultiplier - minFontMultiplier ) / 2; } } while ( ++iterations <= 10 ); } calculateShrinkFit() { const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); // Iterative method to find a fontSize of text children that text will fit into container let iterations = 1; const heightTolerance = 0.075; const firstText = this.childrenInlines.find( inlineComponent => inlineComponent.isText ); let minFontMultiplier = 0; let maxFontMultiplier = 1; let fontMultiplier = firstText._fitFontSize ? firstText._fitFontSize / firstText.getFontSize() : 1; let textHeight; do { textHeight = this.calculateHeight( fontMultiplier ); if ( textHeight > INNER_HEIGHT ) { maxFontMultiplier = fontMultiplier; fontMultiplier -= ( maxFontMultiplier - minFontMultiplier ) / 2; } else { if ( fontMultiplier >= maxFontMultiplier ) { // can't grow text this.childrenInlines.forEach( inlineComponent => { if ( inlineComponent.isInlineBlock ) return; // ensure fontSize does not grow inlineComponent._fitFontSize = inlineComponent.getFontSize(); } ); break; } if ( Math.abs( INNER_HEIGHT - textHeight ) < heightTolerance ) break; minFontMultiplier = fontMultiplier; fontMultiplier += ( maxFontMultiplier - minFontMultiplier ) / 2; } } while ( ++iterations <= 10 ); } calculateAutoFit() { const INNER_HEIGHT = this.getHeight() - ( this.padding * 2 || 0 ); //Iterative method to find a fontSize of text children that text will fit into container let iterations = 1; const heightTolerance = 0.075; const firstText = this.childrenInlines.find( inlineComponent => inlineComponent.isText ); let minFontMultiplier = 0; let maxFontMultiplier = 2; let fontMultiplier = firstText._fitFontSize ? firstText._fitFontSize / firstText.getFontSize() : 1; let textHeight; do { textHeight = this.calculateHeight( fontMultiplier ); if ( textHeight > INNER_HEIGHT ) { maxFontMultiplier = fontMultiplier; fontMultiplier -= ( maxFontMultiplier - minFontMultiplier ) / 2; } else { if ( Math.abs( INNER_HEIGHT - textHeight ) < heightTolerance ) break; if ( Math.abs( fontMultiplier - maxFontMultiplier ) < 5e-10 ) maxFontMultiplier *= 2; minFontMultiplier = fontMultiplier; fontMultiplier += ( maxFontMultiplier - minFontMultiplier ) / 2; } } while ( ++iterations <= 10 ); } /** * computes lines based on children's inlines array. * @private */ computeLines() { // computed by BoxComponent const INNER_WIDTH = this.getWidth() - ( this.padding * 2 || 0 ); // Will stock the characters of each line, so that we can // correct lines position before to merge const lines = [ [] ]; lines.height = 0; const INTERLINE = this.getInterLine(); this.childrenInlines.reduce( ( lastInlineOffset, inlineComponent ) => { // Abort condition if ( !inlineComponent.inlines ) return; ////////////////////////////////////////////////////////////// // Compute offset of each children according to its dimensions ////////////////////////////////////////////////////////////// const FONTSIZE = inlineComponent._fitFontSize || inlineComponent.getFontSize(); const LETTERSPACING = inlineComponent.isText ? inlineComponent.getLetterSpacing() * FONTSIZE : 0; const WHITESPACE = inlineComponent.getWhiteSpace(); const BREAKON = inlineComponent.getBreakOn(); const whiteSpaceOptions = { WHITESPACE, LETTERSPACING, BREAKON, INNER_WIDTH } const currentInlineInfo = inlineComponent.inlines.reduce( ( lastInlineOffset, inline, i, inlines ) => { const kerning = inline.kerning ? inline.kerning : 0; const xoffset = inline.xoffset ? inline.xoffset : 0; const xadvance = inline.xadvance ? inline.xadvance : inline.width; // Line break const shouldBreak = Whitespace.shouldBreak(inlines,i,lastInlineOffset, whiteSpaceOptions ); if ( shouldBreak ) { lines.push( [ inline ] ); inline.offsetX = xoffset; // restart the lastInlineOffset as zero. if ( inline.width === 0 ) return 0; // compute lastInlineOffset normally // except for kerning which won't apply // as there is visually no lefthanded glyph to kern with return xadvance + LETTERSPACING; } lines[ lines.length - 1 ].push( inline ); inline.offsetX = lastInlineOffset + xoffset + kerning; return lastInlineOffset + xadvance + kerning + LETTERSPACING; }, lastInlineOffset ); // return currentInlineInfo; }, 0 ); // Compute lines dimensions let width = 0, height =0, lineOffsetY = -INTERLINE/2; lines.forEach( ( line ) => { // line.lineHeight = line.reduce( ( height, inline ) => { const charHeight = inline.lineHeight !== undefined ? inline.lineHeight : inline.height; return Math.max( height, charHeight ); }, 0 ); // line.lineBase = line.reduce( ( lineBase, inline ) => { const newLineBase = inline.lineBase !== undefined ? inline.lineBase : inline.height; return Math.max( lineBase, newLineBase ); }, 0 ); // line.width = 0; line.height = line.lineHeight; const lineHasInlines = line[ 0 ]; if ( lineHasInlines ) { // starts by processing whitespace, it will return a collapsed left offset const WHITESPACE = this.getWhiteSpace(); const whiteSpaceOffset = Whitespace.collapseWhitespaceOnInlines( line, WHITESPACE ); // apply the collapsed left offset to ensure the starting offset is 0 line.forEach( ( inline ) => { inline.offsetX -= whiteSpaceOffset; } ); // compute its width: length from firstInline:LEFT to lastInline:RIGHT line.width = this.computeLineWidth( line ); if( line.width > width ){ width = line.width; } line.forEach( ( inline ) => { inline.offsetY = (lineOffsetY - inline.height) - inline.anchor; if( inline.lineHeight < line.lineHeight ){ inline.offsetY -= line.lineBase- inline.lineBase; } } ); line.y = lineOffsetY; // line.x will be set by textAlign height += ( line.lineHeight + INTERLINE ); lineOffsetY = lineOffsetY - (line.lineHeight + INTERLINE ); } } ); lines.height = height; lines.width = width; return lines; } calculateHeight( fontMultiplier ) { this.childrenInlines.forEach( inlineComponent => { if ( inlineComponent.isInlineBlock ) return; // Set font size and recalculate dimensions inlineComponent._fitFontSize = inlineComponent.getFontSize() * fontMultiplier; inlineComponent.calculateInlines( inlineComponent._fitFontSize ); } ); const lines = this.computeLines(); return Math.abs( lines.height ); } /** * Compute the width of a line * @param line * @returns {number} */ computeLineWidth( line ) { // only by the length of its extremities const firstInline = line[ 0 ]; const lastInline = line[ line.length - 1 ]; // Right + Left ( left is negative ) return (lastInline.offsetX + lastInline.width) + firstInline.offsetX; } }; }