Skip to content

Commit

Permalink
Trac #30614: Set size, font, and opacity for text3d in Three.js viewer
Browse files Browse the repository at this point in the history
This ticket proposes allowing the user to customize the size, font, and
opacity as well as the bold/italic styling for [https://doc.sagemath.org
/html/en/reference/plot3d/sage/plot/plot3d/shapes2.html#sage.plot.plot3d
.shapes2.text3d text3d] displayed in the Three.js viewer.

Additionally, the font option should support CSS font fallback by
providing an array of font names or a string of comma-separated font
names.

URL: https://trac.sagemath.org/30614
Reported by: gh-jcamp0x2a
Ticket author(s): Joshua Campbell
Reviewer(s): Eric Gourgoulhon
  • Loading branch information
Release Manager committed Oct 4, 2020
2 parents ef0ab12 + 6a6d33e commit 8a6a415
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 14 deletions.
25 changes: 20 additions & 5 deletions src/sage/ext_data/threejs/threejs_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 );
Expand All @@ -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 );

Expand Down Expand Up @@ -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;
}

Expand Down
87 changes: 83 additions & 4 deletions src/sage/plot/plot3d/shapes.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,7 @@ class Text(PrimitiveObject):
"""
PrimitiveObject.__init__(self, **kwds)
self.string = string
self._set_extra_kwds(kwds)

def x3d_geometry(self):
"""
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
24 changes: 20 additions & 4 deletions src/sage/plot/plot3d/shapes2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)::
Expand All @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion src/sage/plot/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 8a6a415

Please sign in to comment.