Splitting text into lines according to maximum width + vertical text scroll in JavaScript and HTML5

Yesterday I made another iOS build (I do them about once a week, but will be ramping up the frequency now) and spent a few hours fixing iOS specific bugs, then trying to consolidate the iOS code with the browser version code to take out as many differences as possible.

In the midst of this I made a new branch for iOS in my repository. And learned about cherry picking (thanks, awesome Twitter people and Google) and more of the ins and outs of SourceTree (the git client I use).

For the iOS version I built simple control buttons. The main goal was to be able to test performance better. I know I can get the game to run, but I didn’t know if it would run well. The answer is, right now, it runs _decently_ but not well enough. I am definitely going to have to work on this to get it up to scratch. Performance is almost perfect until you get more than a couple of entities on the screen, at which point you see some pretty noticeable jitter.

Positioning and scaling GUI elements in relation to game window

I then decided to go ahead and start sorting out some of the other iOS bugs. The first thing was modifying the mission brief screen to space and size elements correctly for different iOS devices. Because the resolution on different iPhones varies, the game window sizes itself dynamically to each device (I am only focusing on iPhone 4, 4S, and 5 here). This means that if I size everything for the iPhone 5 resolution, elements will become truncated and not display correctly on the iPhone 4/4S screen. This is exactly what was happening because my placeholder GUI screens and buttons were quickly hacked together in Photoshop. Today I picked this apart and positioned each element in the code instead of inside the actual image. This allowed me position text and clickable elements in relation to the size of the game window.

It’s still really messed up and the positions are way off (as well as many of the font sizes, which I’ve been revising for the iPhone screen), but the main thing is that I can now rearrange and resize these elements as needed, even if the overall layout changes.

[caption id=“attachment_49060” align=“aligncenter” width=“300”]Ejecta on iOS Dodgy/unfinished text size and spacing on iOS[/caption]

Because of this, I knew I would run into difficulties with displaying text. Screens have to accommodate for different text being pulled into them for each mission (especially things like the mission brief). In addition with the font sizes having to be larger for the iPhone screen, there’s just not much room to fit everything that I may_ _need to display. I’m trying to minimize the amount of text I use in general as I don’t want this to get overly verbose, but I knew I’d need to think of something to make this thing scalable for larger text blocks.

I’ve been setting line breaks manually in my code by splitting strings every time a ‘~’ character appears. Each tilda signified a new line. The thing is depending on screen size, the line width would also be different. I decided to try to split each string “properly”. While I was at it I decided to make my mission brief text area into a scroll box.

So I made something that looks like this in its roughest form:

[caption id=“attachment_49061” align=“aligncenter” width=“300”]Text scroll and line breaks in Ejecta on iOS Scroll bars for text that breaks itself into lines depending on max width, which is set according to game window width.[/caption]

Splitting a string into multiple lines based on maximum line width

First, I made a rough entity dedicated solely to displaying scrollable text. Right now I spawn this entity from my GUI entity on the Mission Brief level. The main GUI entity passes through the actual text that needs to be put into the text box:

            this.missionText = ig.game.Controller.setMissionBrief();
            var briefSize = {x: ig.system.width - 180, y: 210};
            var briefPos = {x: 170, y: 100};
            ig.game.spawnEntity(EntityScrollablegui, 0, 0, {text: this.missionText.brief, font: this.font15, size: {x: briefSize.x, y: briefSize.y}, pos: {x: briefPos.x, y: briefPos.y}, image: this.captPortrait});

EntityScrollablegui then breaks the text down into lines depending on its own size (which as you can see is dependent on the size of the screen). This is done in the init() function when it’s spawned:

        this.textPos = {x: this.pos.x + 5, y: this.pos.y + 30};
        this.maxTextWidth = this.size.x - 105;
        this.lineheight = 20;
        this.upArrowPos = {x: this.pos.x + this.size.x - 90, y: this.pos.y + 5};
        this.downArrowPos = {x: this.pos.x + this.size.x - 90, y: this.pos.y + this.size.y - 90}
        this.allLines = ig.game.Controller.splitLines(this.text,this.font,this.maxTextWidth);

        ig.game.spawnEntity(EntityTrigger, this.upArrowPos.x, this.upArrowPos.y, {kind: 'upArrow', cursorFade: true, parentEntity: this} );
        ig.game.spawnEntity(EntityTrigger, this.downArrowPos.x, this.downArrowPos.y, {kind: 'downArrow', cursorFade: true, parentEntity: this} );

As you can see I still have some things that are hard coded here which shouldn’t be, such as line height (as this can vary depending on font size). I’ll need to refine all of this later.

Right now the important part is the splitLines() function, which is in the custom Controller class and looks like this (over commented for this post):

    splitLines: function(text,font,maxTextWidth) {
        // Split text into words by spaces
        var words = text.split(' ');
        var lastWord = words[words.length - 1];
        var lineWidth = 0;
        var wordWidth = 0;
        var thisLine = '';
        var allLines = new Array();

        // For every element in the array of words
        for (var i = 0; i < words.length; i++) {
            var word = words[i];
            // Add current word to current line
            thisLine = thisLine.concat(word + ' ');
            // Get width of the entire current line
            lineWidth = font.getWidth(thisLine);
            // If word is not the last element in the array
            if (word !== lastWord) {
                // Find out what the next upcoming word is
                var nextWord = words[i + 1];

                // Check if the current line + the next word would go over width limit
                if (lineWidth + font.getWidth(nextWord) >= maxTextWidth) {
                    // If so, add the current line to the allLines array
                    // without adding the next word
                    addToAllLines(thisLine);
                } 

                // '~' indicates inserting a blank line, if required
                else if (word === '~') {
                    addToAllLines(' ');
                }

                // If the next word is a line break, end line now
                else if (nextWord === '~') {
                    addToAllLines(thisLine);
                }
            }

            // If this IS the last word in the array
            else {
                // Add this entire line to the array and return allLines
                addToAllLines(thisLine);
                return allLines;
            }
        }

        // Function that adds text to the array of all lines
        function addToAllLines(text) {
            allLines.push(text);
            thisLine = '';
            lineWidth = 0;
        }       
    },

Vertical text scroll

So now we have an array of lines of the right width to print out. Back in the scrollable text entity, this happens in the draw() function. Again, over-commented for this post:

    draw: function() {
        this.parent();

        // Draw portrait of speaker
        if (this.image) {
            this.image.draw(this.pos.x - 150,this.pos.y);
        }

        // Draw text area box
        ig.system.context.fillStyle = 'rgba(0,0,0' + 0.4 +')';
        ig.system.context.fillRect(this.pos.x, this.pos.y, this.size.x, this.size.y);

        var alpha = 0.5;    
        var x = this.textPos.x;
        var y = this.textPos.y - this.lineheight;

        // For each element in allLines array
        for (var i = 0; i < this.allLines.length; i++) {
            var line = this.allLines[i];
            y += this.lineheight;
            // If the line is too high or too low, skip this iteration
            if (y < this.pos.y + 30 || y > this.pos.y + this.size.y - 25) {
                continue;
            }

            // Draw line
            ig.game.gui.font15.draw( line, x, y, 'left', 'rgba(255,255,255,' + alpha + ')');
        } 

        // Draw up and down arrows for scrolling
        this.upArrow.draw(this.upArrowPos.x, this.upArrowPos.y);
        this.downArrow.draw(this.downArrowPos.x, this.downArrowPos.y);

    }

The text area ends up looking something like this:

iOS JavaScript scrolling text

To actually make the up and down arrows work I use the two triggers that were spawned in this entity’s earlier init(). In trigger.js I check the ‘kind’ of the trigger when it is clicked and do something accordingly. The if statements limit further scrolling if the user has already reached the top or bottom lines of the text:

                case 'upArrow':
                    if (this.parentEntity.textPos.y < this.parentEntity.pos.y + 30) {
                        this.parentEntity.textPos.y += this.parentEntity.lineheight;
                    }
                    break;
                case 'downArrow':
                    if (this.parentEntity.textPos.y + (this.parentEntity.lineheight * this.parentEntity.allLines.length) > this.parentEntity.pos.y + this.parentEntity.size.y - 25) {
                        this.parentEntity.textPos.y -= this.parentEntity.lineheight;
                    }
                    break;

And that’s it. I tested this in iOS with Ejecta as well as in a regular browser_. _It’s most likely not the best solution and I’ll keep making improvements as I go, but it works for now.

comments powered by Disqus