MeasureString and font scaling.

Hi folks.

I’ve been playing about with the GUI part of the game I’m working on. I have a Box class which basically draws a rectangle but has a load of properties to display information in the space, one of which is text.

If I use the MeasureString function on text displayed 1:1 it’s fine, however if I display text at .5 size and multiply the MeasureString by the same variable the text moves right and down,

Has anyone else found this?

I can’t see how my calculations can be wrong, all I’m doing is multiplying the measure result by the ratio then divide it by 2 to find the centre point. For testing I’m using a very standard Tahoma that’s been ‘spritefonted’.

I could spriteify loads of different sizes of font, but that seems wasteful.

Any ideas?
Thanks.

@MuntyScruntFundle maybe (not tested)
you can just do: MeasureString(YOURTEXTHERE) * SIZEOFYOURFONT
i dont know, it woud be the first thing i thought of here…

MeasureString returns the width and height of text at its 1/1 point size.
MeasureString returns the width and height of multiple lines of text as well as the total.

for example if each of the below letters were 10x10 pixels and you sent the following lines to MeasureString.

A
BBBBB
CC

MeasureString would return a size of 50,30

This is not useful for positioning of a rectangles x,y position elements directly.

It’s not all that great of a function nor is it efficient. I did some experiments that allowed a great deal of, ‘on the fly’ manipulation to text however unfortunately i couldn’t get the same speed.

I could use / post those classes and share them if spritebatch had a full overload that allowed one to pass vertice positions LT RT RB LB of a quad to spritebatch it really should i don’t know why we don’t have a direct overload that allows you to pass directly the vertice positions. All that is just extra overhead anyways that is done by spritebatch.DrawRectangle.

It’s just that no one implemented it yet. This came up on GitHub in a discussion before. I’m sure a good PR that implements this behavior would be accepted :slight_smile:

Have you tried not doing that? I thing the origin is in relation to the 1/1 text size.

[quote=“willmotil, post:3, topic:9366”]
I could use / post those classes and share them if spritebatch had a full overload that allowed one to pass vertice positions LT RT RB LB of a quad to spritebatch it really should i don’t know why we don’t have a direct overload that allows you to pass directly the vertice positions.[/quote]

Which part exactly is the bottleneck? How would you take advance of a SpriteBatch that lets you batch primitives to draw text faster?

SpriteBatch is specialized at drawing rectangles/quads. The only reason I see in passing vertices is for arbitrary shapes/polygons, but then it will have to accept any polygon size, at entire different request IMO.

Sorry for the late reply i didn’t see this.

How would you take advance of a SpriteBatch that lets you batch primitives to draw text faster?

I have quite a few methods for cutting text and manipulating text on the fly. Including a couple extra classes that do a little more advanced manipulation on words or paragraphs and colors.

Most of these rely on being able to directly define vertice edges of the quads the colors of the quads, command text within the strings passed themselves avoiding MeasureString in the drawString command precomputing rotations on the fly ect.

For example in one method i call this in my own drawstring.

spriteBatch.Draw(tsf.Texture, dest, currentGlyph.BoundsInTexture, color, rotation, Vector2.Zero, SpriteEffects.None, depth);

If i am need of a overload that allows me to set the vertices directly.
Then i already know what the 2d representation of the quad is on screen 3 dimensionally.

All of the below bolded feilds are then redundant in the draw method but will be recomputed anyways.

spriteBatch.Draw(tsf.Texture, dest, currentGlyph.BoundsInTexture, color, rotation, Vector2.Zero, SpriteEffects.None, depth);

Further the dest and bounds will be recalculated to floats in that draw but i have just calculated the integers from floats to pass to that method. I also know the color per glyph, i may even know the color per vertice i want for the text to give off a cool cheap effect or highlight some words. I may want to simulate italicized text by drawing a quadrilateral quad instead of a directly square one ect.

Spritebatcher is very efficient at this point and its limited to squares that are all of the same color.
However this limitation is in fact completely redundant and not really neccessary, from its wrapper class Spritebatch’s and its Draw overloads this can actually be inefficient in some cases. It also is pointless reduced functionality there is no reason Draw should only take squares it is certainly not for performance.

I don’t have a way to efficiently bypass the extra junk in draw to basically give a quad to spriteBatcher which would be nice.

Anyways…

To keep on topic of the post i made this little class for cutting strings today.
I figured id post it to share or for posterity.

This just cuts down and returns a string or gives the index for a string for were it is to get cut off by a bounding rectangles width. This should work with scaling as well, its very simple it uses the same algorithm that DrawString itself uses though this might be a bit simplified.

I didn’t include cutting it off for the bounds height though its easy to add it yourself if you want.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;


namespace Microsoft.Xna.Framework
{

    public static class TextMeasure
    {
        private static SpriteFont tsf;
        private static Dictionary<char, SpriteFont.Glyph> _glyphs;
        private static SpriteFont.Glyph defaultGlyph;
        private static char defaultfontchar = ' ';

        public static void SetSpriteFont(SpriteFont s)
        {
            tsf = s;
            _glyphs = tsf.GetGlyphs();
            defaultGlyph = new SpriteFont.Glyph();
            if (tsf.DefaultCharacter.HasValue)
            {
                defaultfontchar = (char)(tsf.DefaultCharacter.Value);
                defaultGlyph = _glyphs[defaultfontchar];
            }
        }

        public static string CutStringByBounds(string text, Rectangle boundRect)
        {
            var i = CutStringsEndingCharIndex(text, Vector2.One, boundRect);
            return text.Substring(0, i);
        }
        public static string CutStringByBounds(SpriteFont sf, string text, Rectangle boundRect)
        {
            SetSpriteFont(sf);
            var i = CutStringsEndingCharIndex(text, Vector2.One, boundRect);
            return text.Substring(0, i);
        }
        public static string CutStringByBounds(SpriteFont sf, string text, Vector2 scale, Rectangle boundRect)
        {
            SetSpriteFont(sf);
            var i = CutStringsEndingCharIndex(text, scale, boundRect);
            return text.Substring(0, i);
        }
        public static string CutStringByBounds(string text, Vector2 scale, Rectangle boundRect)
        {
            var i = CutStringsEndingCharIndex(text, scale, boundRect);
            return text.Substring(0, i);
        }


        public static int CutStringsEndingCharIndex(string text, Rectangle boundRect)
        {
            return CutStringsEndingCharIndex(text, Vector2.One, boundRect);
        }
        public static int CutStringsEndingCharIndex(SpriteFont sf, string text, Rectangle boundRect)
        {
            SetSpriteFont(sf);
            return CutStringsEndingCharIndex(text, Vector2.One, boundRect);
        }
        public static int CutStringsEndingCharIndex(SpriteFont sf, string text, Vector2 scale, Rectangle boundRect)
        {
            SetSpriteFont(sf);
            return CutStringsEndingCharIndex(text, scale, boundRect);
        }

        public static int CutStringsEndingCharIndex(string text, Vector2 scale, Rectangle boundRect)
        {
            var lineHeight = (float)tsf.LineSpacing;
            var Spacing = tsf.Spacing;
            Vector2 offset = Vector2.Zero;
            Rectangle dest = new Rectangle();
            var currentGlyph = SpriteFont.Glyph.Empty;
            var firstGlyphOfLine = true;

            int result = 0;

            for (var i = 0; i < text.Length; i++)
            {
                var c = text[i];
                if (c == '\r')
                    continue;
                if (c == '\n')
                {
                    offset.X = 0;
                    offset.Y += lineHeight;
                    firstGlyphOfLine = true;
                    continue;
                }

                if (_glyphs.ContainsKey(c))
                    currentGlyph = _glyphs[c];
                else
                    if (!tsf.DefaultCharacter.HasValue)
                        throw new ArgumentException("Text Contains a Unresolvable Character");
                    else
                        currentGlyph = defaultGlyph;

                // Solves the problem- the first character on a line with a negative left side bearing.
                if (firstGlyphOfLine)
                {
                    offset.X = Math.Max(currentGlyph.LeftSideBearing, 0);
                    firstGlyphOfLine = false;
                }
                else
                    offset.X += Spacing + currentGlyph.LeftSideBearing;

                // matrix calculations unrolled removed un-needed here
                var m = offset;
                m.X += currentGlyph.Cropping.X;
                m.Y += currentGlyph.Cropping.Y;

                dest = new Rectangle(
                    (int)(m.X * scale.X),
                    (int)(m.Y * scale.Y),
                    (int)(currentGlyph.BoundsInTexture.Width * scale.X),
                    (int)(currentGlyph.BoundsInTexture.Height * scale.Y)
                    );

                if (dest.Right < boundRect.Width)
                {
                    result = i + 1;
                }
                else
                {
                    return result;
                }
                offset.X += currentGlyph.Width + currentGlyph.RightSideBearing;
            }
            return result;
        }
    }
}

Its used like so…

text = TextMeasure.CutStringByBounds(font, text, textBoundingRectangle);