diff --git a/src/sage/ext_data/threejs/threejs_template.html b/src/sage/ext_data/threejs/threejs_template.html index 62dc0c44a93..83a4efe386a 100644 --- a/src/sage/ext_data/threejs/threejs_template.html +++ b/src/sage/ext_data/threejs/threejs_template.html @@ -108,15 +108,18 @@ } - function addLabel( text, x, y, z, color='black', fontsize=14 ) { + function addLabel( text, x, y, z, color='black', fontSize=14, fontFamily='monospace', + fontStyle='normal', fontWeight='normal', opacity=1 ) { var canvas = document.createElement( 'canvas' ); var context = canvas.getContext( '2d' ); var pixelRatio = Math.round( window.devicePixelRatio ); - context.font = fontsize + 'px monospace'; + var font = [fontStyle, fontWeight, fontSize + 'px', fontFamily].join(' '); + + context.font = font; var width = context.measureText( text ).width; - var height = fontsize; + var height = fontSize; // The dimensions of the canvas's underlying image data need to be powers // of two in order for the resulting texture to support mipmapping. @@ -132,7 +135,7 @@ context.scale( pixelRatio, pixelRatio ); context.fillStyle = color; - context.font = fontsize + 'px monospace'; // Must be set again after measureText. + context.font = font; // Must be set again after measureText. context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillText( text, width/2, height/2 ); @@ -141,6 +144,12 @@ texture.needsUpdate = true; var materialOptions = { map: texture, sizeAttenuation: false }; + if ( opacity < 1 ) { + // Setting opacity=1 would cause the texture's alpha component to be + // discarded, giving the text a black background instead of the + // background being transparent. + materialOptions.opacity = opacity; + } var sprite = new THREE.Sprite( new THREE.SpriteMaterial( materialOptions ) ); sprite.position.set( x, y, z ); @@ -247,7 +256,13 @@ for ( var i=0 ; i < texts.length ; i++ ) addText( texts[i] ); function addText( json ) { - var sprite = addLabel( json.text, a[0]*json.x, a[1]*json.y, a[2]*json.z, json.color ); + json.fontFamily = json.fontFamily.map( function( f ) { + // Need to put quotes around fonts that have whitespace in their names. + return /\s/.test( f ) ? '"' + f + '"' : f; + }).join(', '); + var sprite = addLabel( json.text, a[0]*json.x, a[1]*json.y, a[2]*json.z, json.color, + json.fontSize, json.fontFamily, json.fontStyle, json.fontWeight, + json.opacity ); sprite.userData = json; } diff --git a/src/sage/plot/plot3d/shapes.pyx b/src/sage/plot/plot3d/shapes.pyx index 6b6150ed5ab..85903ec3b4a 100644 --- a/src/sage/plot/plot3d/shapes.pyx +++ b/src/sage/plot/plot3d/shapes.pyx @@ -1093,6 +1093,7 @@ class Text(PrimitiveObject): """ PrimitiveObject.__init__(self, **kwds) self.string = string + self._set_extra_kwds(kwds) def x3d_geometry(self): """ @@ -1168,9 +1169,21 @@ class Text(PrimitiveObject): EXAMPLES:: - sage: T = text3d("Hi", (1, 2, 3), color='red') + sage: T = text3d("Hi", (1, 2, 3), color='red', fontfamily='serif', + ....: fontweight='bold', fontstyle='italic', fontsize=20, + ....: opacity=0.5) sage: T.threejs_repr(T.default_render_params()) - [('text', {'color': '#ff0000', 'text': 'Hi', 'x': 1.0, 'y': 2.0, 'z': 3.0})] + [('text', + {'color': '#ff0000', + 'fontFamily': ['serif'], + 'fontSize': 20.0, + 'fontStyle': 'italic', + 'fontWeight': 'bold', + 'opacity': 0.5, + 'text': 'Hi', + 'x': 1.0, + 'y': 2.0, + 'z': 3.0})] TESTS: @@ -1180,15 +1193,81 @@ class Text(PrimitiveObject): sage: from sage.plot.plot3d.shapes import Text sage: T = Text("Hi") sage: T.threejs_repr(T.default_render_params()) - [('text', {'color': '#6666ff', 'text': 'Hi', 'x': 0.0, 'y': 0.0, 'z': 0.0})] + [('text', + {'color': '#6666ff', + 'fontFamily': ['monospace'], + 'fontSize': 14.0, + 'fontStyle': 'normal', + 'fontWeight': 'normal', + 'opacity': 1.0, + 'text': 'Hi', + 'x': 0.0, + 'y': 0.0, + 'z': 0.0})] """ center = (float(0), float(0), float(0)) if render_params.transform is not None: center = render_params.transform.transform_point(center) + color = '#' + str(self.texture.hex_rgb()) string = str(self.string) - text = dict(text=string, x=center[0], y=center[1], z=center[2], color=color) + + default_size = 14.0 + size = self._extra_kwds.get('fontsize', default_size) + try: + size = float(size) + except (TypeError, ValueError): + scale = str(size).lower() + if scale.endswith('%'): + try: + scale = float(scale[:-1]) / 100.0 + size = default_size * scale + except ValueError: + import warnings + warnings.warn(f"invalid fontsize: {size}, using: {default_size}") + size = default_size + else: + from matplotlib.font_manager import font_scalings + try: + size = default_size * font_scalings[scale] + except KeyError: + import warnings + warnings.warn(f"unknown fontsize: {size}, using: {default_size}") + size = default_size + + font = self._extra_kwds.get('fontfamily', ['monospace']) + if isinstance(font, str): + font = font.split(',') + font = [str(f).strip() for f in font] + + default_style = 'normal' + style = str(self._extra_kwds.get('fontstyle', default_style)) + if style not in ['normal', 'italic'] and not style.startswith('oblique'): # ex: oblique 30deg + import warnings + warnings.warn(f"unknown style: {style}, using: {default_style}") + style = default_style + + default_weight = 'normal' + weight = self._extra_kwds.get('fontweight', default_weight) + if weight not in ['normal', 'bold']: + try: + weight = int(weight) + except: + from matplotlib.font_manager import weight_dict + try: + weight = weight_dict[weight] + except KeyError: + import warnings + warnings.warn(f"unknown fontweight: {weight}, using: {default_weight}") + weight = default_weight + + opacity = float(self._extra_kwds.get('opacity', 1.0)) + + text = dict(text=string, x=center[0], y=center[1], z=center[2], color=color, + fontSize=size, fontFamily=font, fontStyle=style, fontWeight=weight, + opacity=opacity) + return [('text', text)] def bounding_box(self): diff --git a/src/sage/plot/plot3d/shapes2.py b/src/sage/plot/plot3d/shapes2.py index 07e00c048da..3841c762de0 100644 --- a/src/sage/plot/plot3d/shapes2.py +++ b/src/sage/plot/plot3d/shapes2.py @@ -670,10 +670,6 @@ def text3d(txt, x_y_z, **kwds): - ``**kwds`` -- standard 3d graphics options - .. note:: - - There is no way to change the font size or opacity yet. - EXAMPLES: We write the word Sage in red at position (1,2,3):: @@ -696,6 +692,26 @@ def text3d(txt, x_y_z, **kwds): sage: text3d("Sage is...",(2,12,1), color=(1,0,0)) + text3d("quite powerful!!",(4,10,0), color=(0,0,1)) Graphics3d Object + + Adjust the font size, family, style, and weight (Three.js viewer only):: + + sage: t0 = text3d("Pixel size", (0, 0, 0), fontsize=20) + sage: t1 = text3d("Percentage size", (0, 0, 1), fontsize='300%') + sage: t2 = text3d("Keyword size", (0, 0, 2), fontsize='x-small') + sage: t3 = text3d("Single family", (0, 0, 3), fontfamily='serif') + sage: t4 = text3d("Family fallback", (0, 0, 4), fontfamily=['Consolas', 'Lucida Console', 'monospace']) + sage: t5 = text3d("Another way", (0, 0, 5), fontfamily='Consolas, Lucida Console, monospace') + sage: t6 = text3d("Style", (0, 0, 6), fontstyle='italic') + sage: t7 = text3d("Keyword weight", (0, 0, 7), fontweight='bold') + sage: t8 = text3d("Integer weight (1-1000)", (0, 0, 8), fontweight=800) # 'extra bold' + sage: sum([t0, t1, t2, t3, t4, t5, t6, t7, t8]).show(viewer='threejs', frame=False) + + Adjust the text's opacity (Three.js viewer only):: + + sage: def echo(o): + ....: return text3d("Echo!", (0, 0, o), opacity=o) + sage: show(sum([echo(o) for o in (0.1, 0.2, .., 1)]), viewer='threejs') + """ (x, y, z) = x_y_z if 'color' not in kwds and 'rgbcolor' not in kwds: diff --git a/src/sage/plot/text.py b/src/sage/plot/text.py index 600c7c8b1cb..04cbdedf768 100644 --- a/src/sage/plot/text.py +++ b/src/sage/plot/text.py @@ -138,8 +138,11 @@ def _plot3d_options(self, options=None): if options is None: options = dict(self.options()) options_3d = {} + for s in ['fontfamily', 'fontsize', 'fontstyle', 'fontweight']: + if s in options: + options_3d[s] = options.pop(s) # TODO: figure out how to implement rather than ignore - for s in ['axis_coords', 'clip', 'fontsize', 'horizontal_alignment', + for s in ['axis_coords', 'clip', 'horizontal_alignment', 'rotation', 'vertical_alignment']: if s in options: del options[s]