/**
* @module SpriteText
* @description A sprite based text component. Text that always faces towards the camera.
* @see {@link https://threejs.org/docs/index.html#api/en/objects/Sprite|THREE.Sprite}
*
* @author Andrej Hristoliubov. {@link https://anhr.github.io/AboutMe/|AboutMe}
*
* @copyright 2011 Data Arts Team, Google Creative Lab
*
* @license under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
//var THREE;
import three from '../three.js'
/**
* A sprite based text component.
* @param {string|number} text The text to be displayed on the sprite. You can include a multiline text separated by "\r\n".
* @param {THREE.Vector3} [position=new THREE.Vector3()] Position of the text.
* @param {object} [options={}] the following options are available
* @param {string} [options.name] Name of the <b>SpriteText</b> instance.
* @param {THREE.Group} [options.group] Parent group of the SpriteText with common options.
* See {@link https://github.com/anhr/SpriteText#groupuserdataoptionsspritetext---common-options-for-the-group-of-the-spritetext|common options for the group of the SpriteText}.
* Default is undefined.
* @param {number} [options.textHeight=0.04] The height of the text.
* @param {number} [options.fov] Camera frustum vertical field of view, from bottom to top of view, in degrees.
* {@link https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera.fov|PerspectiveCamera.fov}
* Set the fov option as camera.fov if you want to see text size is independent from camera.fov. The text height will be calculated as textHeight = fov * textHeight / 50
* Default is undefined.
* @param {boolean} [options.sizeAttenuation=false] Whether the size of the sprite is attenuated by the camera depth. (Perspective camera only.)
* See {@link https://threejs.org/docs/index.html#api/en/materials/SpriteMaterial.sizeAttenuation|SpriteMaterial.sizeAttenuation}
* @param {number} [options.rotation=0] The rotation of the sprite in radians.
* See {@link https://threejs.org/docs/index.html#api/en/materials/SpriteMaterial.rotation|SpriteMaterial.rotation}
* @param {string} [options.fontFace='Arial'] CSS font-family - specifies the font of the text.
* @param {string[]} [options.fontFaces] array of fontFaces. Example ['Arial', 'Verdana', 'Times'].
* @param {string} [options.fontColor='rgba(255, 255, 255, 1)'] RGBA object or RGB object or HEX value.
* Examples 'rgba(0, 0, 255, 0.5)', '#00FF00'.
* @param {boolean} [options.bold=false] CSS font-weight. Equivalent of 700.
* @param {boolean} [options.italic=false] CSS font-style.
* @param {string} [options.fontProperties] Other font properties. The font property uses the same syntax as the CSS font property.
* Default is empty string. Example "900", "oblique lighter".
* @param {object} [options.rect={}] rectangle around the text.
* @param {boolean} [options.rect.displayRect=false] true - the rectangle around the text is visible.
* @param {string} [options.rect.backgroundColor='rgba(0, 0, 0, 0)' - black transparent] background color. RGBA object or RGB object or HEX value
* <pre>
* Examples 'rgba(0, 0, 255, 0.5)', '#00FF00'.
* </pre>
* @param {string} [options.rect.borderColor] border color. RGBA object or RGB object or HEX value. Default is same as options.fontColor 'rgba(255, 255, 255, 1)' - white.
* @param {number} [options.rect.borderThickness=0 is invisible border] border thickness.
* @param {number} [options.rect.borderRadius=0 is no radius] border corners radius.
* @param {THREE.Vector2|object} [options.center] If <b>center.x</b> and <b>center.y</b> is defined, then it the text's anchor point.
* <pre>
* See {@link https://threejs.org/docs/index.html#api/en/objects/Sprite.center|Sprite.center}
* A value of (0.5, 0.5) corresponds to the midpoint of the text.
* A value of (0, 0) corresponds to the left lower corner of the text.
* A value of (0, 1) corresponds to the left upper corner of the text.
*
* Otherwise, the center is calculated so that the text is always inside the canvas.
* Please define <b>center.camera</b> and <b>center.canvas</b> for it. See below for details.
*
* If <b>options.center</b> is not defined, center is left upper corner: <b>new THREE.Vector2( 0, 1 )</b>
* </pre>
* @param {THREE.PerspectiveCamera} [center.camera] [PerspectiveCamera]{@link https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera}
* @param {HTMLElement} [center.canvas] <b>canvas</b> element.
* @see Thanks to {@link https://github.com/vasturiano/three-spritetext|three-spritetext}
*/
export function SpriteText( text, position, options = {} ) {
const THREE = three.THREE;
position = position || new THREE.Vector3( 0, 0, 0 );
const sprite = new THREE.Sprite( new THREE.SpriteMaterial( {
map: new THREE.Texture(),
sizeAttenuation: options.sizeAttenuation !== undefined ? options.sizeAttenuation :
false,//The size of the sprite is not attenuated by the camera depth. (Perspective camera only.)
} ) );
const canvas = document.createElement( 'canvas' );
sprite.material.map.minFilter = THREE.LinearFilter;
const fontSize = 90;
const context = canvas.getContext( '2d' );
if ( options.name ) sprite.name = options.name;
sprite.userData.update = function ( /*optionsUpdate*/ ) {
const optionsUpdate = {};
if ( sprite.parent )
updateOptions( sprite.parent, optionsUpdate );
else if ( options.group )
updateOptions( options.group, optionsUpdate );
var textHeight = options.textHeight || optionsUpdate.textHeight || 0.04;
const fov = options.fov || optionsUpdate.fov,
sizeAttenuation = options.sizeAttenuation || optionsUpdate.sizeAttenuation || false,
rotation = options.rotation || optionsUpdate.rotation || 0,
fontFace = options.fontFace || optionsUpdate.fontFace || 'Arial',
bold = options.bold || optionsUpdate.bold || false,
italic = options.italic || optionsUpdate.italic || false,
fontProperties = options.fontProperties || optionsUpdate.fontProperties || '',
rect = options.rect || optionsUpdate.rect || {},
color = 'rgba(255, 255, 255, 1)',
fontColor = options.fontColor || optionsUpdate.fontColor || color,
center = SpriteText.getCenter( options.center || optionsUpdate.center, position );
if ( fov !== undefined )
textHeight = fov * textHeight / 50;
rect.displayRect = rect.displayRect || false;
const borderThickness = rect.borderThickness ? rect.borderThickness : 5,
font = `${fontProperties}${bold ? 'bold ' : ''}${italic ? 'italic ' : ''}${fontSize}px ${fontFace}`;
context.font = font;
var width = 0, linesCount = 1,
lines;
if ( typeof text === 'string' ) {
linesCount = 0;
lines = text.split( /\r\n|\r|\n/ );
lines.forEach( function ( line ) {
var lineWidth = context.measureText( line ).width;
if ( width < lineWidth )
width = lineWidth;
linesCount += 1;
} );
} else width = context.measureText( text ).width;
width += borderThickness * 2;
const textWidth = width;
canvas.width = textWidth;
canvas.height = fontSize * linesCount + borderThickness * 2;
context.font = font;
//Rect
//Thanks to http://stemkoski.github.io/Three.js/Sprite-Text-Labels.html
if ( rect.displayRect ) {
// background color
context.fillStyle = rect.backgroundColor ? rect.backgroundColor : 'rgba(0, 0, 0, 1)';
// border color
context.strokeStyle = rect.borderColor ? rect.borderColor : fontColor;
context.lineWidth = borderThickness;
// function for drawing rounded rectangles
function roundRect( ctx, x, y, w, h, r ) {
ctx.beginPath();
ctx.moveTo( x + r, y );
ctx.lineTo( x + w - r, y );
ctx.quadraticCurveTo( x + w, y, x + w, y + r );
ctx.lineTo( x + w, y + h - r );
ctx.quadraticCurveTo( x + w, y + h, x + w - r, y + h );
ctx.lineTo( x + r, y + h );
ctx.quadraticCurveTo( x, y + h, x, y + h - r );
ctx.lineTo( x, y + r );
ctx.quadraticCurveTo( x, y, x + r, y );
ctx.closePath();
ctx.fill();
ctx.stroke();
}
roundRect( context,
borderThickness / 2,
borderThickness / 2,
canvas.width - borderThickness,
canvas.height - borderThickness,
rect.borderRadius === undefined ? 0 : rect.borderRadius
);
}
context.fillStyle = fontColor;
context.textBaseline = 'bottom';
if ( linesCount > 1 ) {
for ( var i = 0; i < lines.length; i++ ) {
const line = lines[i];
context.fillText( line, borderThickness, canvas.height - ( ( lines.length - i - 1 ) * fontSize ) - borderThickness );
}
} else context.fillText( text, borderThickness, canvas.height - borderThickness );
// Inject canvas into sprite
sprite.material.map.image = canvas;
sprite.material.map.needsUpdate = true;
const th = textHeight * linesCount;// * angleDistance;
sprite.scale.set( th * canvas.width / canvas.height, th );
sprite.position.copy( position );
sprite.center = center;
//size attenuation. Whether the size of the sprite is attenuated by the camera depth. (Perspective camera only.) Default is false.
//See https://threejs.org/docs/index.html#api/en/materials/SpriteMaterial.sizeAttenuation
sprite.material.sizeAttenuation = sizeAttenuation;
sprite.material.rotation = rotation;
sprite.material.needsUpdate = true;
};
sprite.userData.update();
sprite.userData.updateText = function ( _text ) {
text = _text;
const options = {}
updateOptions( sprite.parent, options );
sprite.userData.update( options );
}
if ( options.group )
options.group.add( sprite );
sprite.userData.optionsSpriteText = options;
return sprite;
};
/**
* @namespace
* @description Returns {@link https://threejs.org/docs/index.html#api/en/objects/Sprite.center|center}
* @param {THREE.Vector2|object} center If <b>center.x</b> and <b>center.y</b> is defined, then it the text's anchor point.
* <pre>
* Otherwise, the center is calculated so that the text is always inside the canvas.
* Please define <b>center.camera</b> and <b>center.canvas</b> for it. See below for details.
* </pre>
* @param {THREE.PerspectiveCamera} [center.camera] [PerspectiveCamera]{@link https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera}
* @param {HTMLElement} [center.canvas] <b>canvas</b> element.
* @param {THREE.Vector3} [position] Position of the text. Uses only if <b>center.camera</b> and <b>center.canvas</b> is defined
* @returns center. If <b>center</b> is not defined, returns the left upper corner: <b>new THREE.Vector2( 0, 1 )</b>;
*/
SpriteText.getCenter = function ( center = {}, position ) {
const THREE = three.THREE;
const canvas = center.canvas ? center.canvas : undefined;
/**
* Converting World coordinates to Screen coordinates
* https://stackoverflow.com/questions/11586527/converting-world-coordinates-to-screen-coordinates-in-three-js-using-projection
*/
function worldToScreen() {
const width = canvas.width, height = canvas.height,
widthHalf = width / 2, heightHalf = height / 2;
// pos = position.clone();
const pos = new THREE.Vector3().copy( position );
pos.project( center.camera );
pos.x = ( pos.x * widthHalf ) + widthHalf;
pos.y = - ( pos.y * heightHalf ) + heightHalf;
return pos;
}
const screenPos = center.canvas ? worldToScreen() : undefined;
return center instanceof THREE.Vector2 ||
( ( typeof center === "object" ) && ( center.x !== undefined ) && ( center.y !== undefined )//При копироваении и при чтении из cockie THREE.Vector2 превращается в Object{x: x, y: y}
) ? center : screenPos ?
new THREE.Vector2( screenPos.x < ( canvas.width / 2 ) ? 0 : 1, screenPos.y < ( canvas.height / 2 ) ? 1 : 0 ) :
new THREE.Vector2( 0, 1 );//Default is left upper corner
}
function updateOptions( group, options ) {
if ( group.userData.optionsSpriteText )
Object.keys( group.userData.optionsSpriteText ).forEach( function ( key ) {
if ( options[key] === undefined )//Child options have more priority before parent options
options[key] = group.userData.optionsSpriteText[key];
} );
while ( group.parent ) {
group = group.parent;
updateOptions( group, options );
}
}
/**
* @namespace
* @description Call <b>SpriteText.updateSpriteTextGroup</b> if you want to update of the <b>options</b> of all <a href="module-SpriteText.html" target="_blank">SpriteText</a>, added in to <b>group</b> and all <b>child groups</b>.
* @param {THREE.Group|THREE.Scene} group [group]{@link https://threejs.org/docs/index.html?q=gr#api/en/objects/Group} or [scene]{@link https://threejs.org/docs/index.html?q=sce#api/en/scenes/Scene} of the <b>SpriteText</b> and of all <b>child groups</b> of the <b>SpriteText</b> for which these settings will have an effect.
* @example
<script>
optionsSpriteText = {
textHeight: 0.1,
sizeAttenuation: false,
}
const fSpriteTextAll = SpriteTextGui( scene, options, {
settings: { zoomMultiplier: 1.5, },
options: optionsSpriteText
} );
//Change of the text height
optionsSpriteText.textHeight = 0.2;
//update of the options of all SpriteText, added in to group and all child groups
SpriteText.updateSpriteTextGroup( group );
//To do something...
//Restore options.textHeight to 0.1
fSpriteTextAll.userData.restore();
</script>
*/
SpriteText.updateSpriteTextGroup = function( group ) {
const THREE = three.THREE;
group.children.forEach( function ( spriteItem ) {
if ( spriteItem instanceof THREE.Sprite ) {
if ( spriteItem.userData.update !== undefined )
spriteItem.userData.update();
} //else if ( spriteItem instanceof THREE.Group )
SpriteText.updateSpriteTextGroup( spriteItem );
} );
}