Phil Lello
Phil Lello

Reputation: 8639

Scale fixed-width font to fit constant number of characters on a line

I have a websocket based terminal session. I want the font size to increase to fill the div such that it is always 80 characters wide. What's the best way of doing this? I'd love it if there was a CSS-only way of doing this, but I'm already using jQuery, so a vanilla javascript or jQuery solution would be good too.

Upvotes: 9

Views: 4113

Answers (7)

dperish
dperish

Reputation: 1571

I was facing a very similar requirement and came up with a fairly straight forward solution. As soon as I get some down time, I'll be writing a jQuery plugin around this example code.

As was mentioned in some of the other answers here, the key is using em as your font-size unit. Additionally, to achieve a similar layout in all (modern) browsers, for a solution that must cope with various fixed-width fonts, the ruler method is the most reliable approach that I have found.

function fixieRemeasure() {
    var offset =  document.getElementById("fixie").offsetWidth,
        ruler = (document.getElementById("fixieRuler").offsetWidth * 1.01),
        fontSize = (offset / ruler);   
    document.getElementById("fixie").style.fontSize = fontSize + "em";     
}

function fixieInit(columns) {
    document.getElementById("fixieRuler").innerText = 
            new Array(columns + 1).join("X").toString();
    fixieRemeasure();
}

(function () {
    fixieInit(80); 
    window.onresize = function () { fixieRemeasure(); };       
} ());
#fixie {
    padding: 0 2px 0 6px;
    font-size: 1em;
    background-color: #000;
    color: #0F0;
}

#fixieRuler {
    font-size: 1em;
    visibility: hidden;
}
<code id="fixieRuler"></code>

<div id="fixie">
<pre>+------------------------------------------------------------------------------+
|                                                                              |
|                   ,%%%,                                                      |
|                 ,%%%` %==--                                                  |
|                ,%%`( '|                                                      |
|               ,%%@ /\_/                                                      |
|     ,%.-"""--%%% "@@__                                                       |
|    %%/             |__`\                                                     |
|   .%'\     |   \   /  //                                                     |
|   ,%' >   .'----\ |  [/                                                      |
|      < <<`       ||                                                          |
|       `\\\       ||                                                          |
|         )\\      )\                                                          |
| ^^^jgs^^"""^^^^^^""^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        |
|          ASCII Art: http://www.chris.com/ascii/index.php?art=animals/horses  |
+------------------------------------------------------------------------------+</pre>
</div>

This is only a proof of concept and there are a few fairly obvious issues baked in. Mainly, the resize method is called continuously and that should be replaced with something like onResizeEnd and properly attached to the DOM prior to production use. To componentize, I'd use a class selector instead of an id, and would also dynamically insert/remove the ruler element on-the-fly.

Hope it helps.

UPDATE

I created a new project, fixie.js on github, based on this example code. Still has a bit of work to be done in terms of performance and modularization, but it is works well in all browsers and I believe that it provides the simplest means to solve the issue with a minimal amount of boilerplate code.

The only requirements are that you set a font-size to your pre elements in em, and provide them a class name with the format of 'fixie_COLS':

<pre class="fixie_80">I'll be 80 columns</pre>

jQuery & webcomponent implementations to follow. It's MIT licensed and any contributions are welcome: https://github.com/dperish/fixie.js

Upvotes: 0

Rick Hitchcock
Rick Hitchcock

Reputation: 35670

For browsers that support fractional font sizes or CSS text-rendering: geometricPrecision, you can get a fixed-width font to perfectly fit within any bounds.

In this screenshot, the fourth line is exactly 80 characters:

enter image description here

For browsers that don't support fractional font sizes , it is impossible to always make x characters fit within a given width.

Here's the same text with a 12px font:

enter image description here

… and with a 13px font:

enter image description here

Using the 12px font, the longest line is 83 characters, so it's too small. Using the 13px font, the longest line is 77 characters, so it's too large.

In this situation, you must shrink or widen the container to match the font:

enter image description here

The code below places an 80-character wide span in divs of varying widths, using a style of word-wrap: break-word;. It does a binary search for the best font size, using getClientRects() to determine upper and lower bounds for the binary search.

In case the browser doesn't support fractional font sizes, it adjusts the container's width to match the font size.

The span is then removed.

$('div').each(function() {
  var x= $('<span>'+Array(80).join('x')+'</span>').appendTo(this),
      lo= 0.1,
      hi= 50,
      mid;
  do {
    mid= (lo+hi)/2;
    $(this).css({
      fontSize: mid
    });
    if(x[0].getClientRects().length > 1) {
      hi= mid;
    }
    else {
      lo= mid;
    }
  } while(Math.abs(hi-lo)>0.001);
  while(x[0].getClientRects().length === 1) {
    $(this).css({
      width: '-=1'
    });
  }
  while(x[0].getClientRects().length === 2) {
    $(this).css({
      width: '+=1'
    });
  }
  x.remove();
});

Tested in Chrome, IE, Firefox, Safari, and Opera.

Fiddle

Upvotes: 4

Shalom Sam
Shalom Sam

Reputation: 1559

This can be done using pure css using the vw (viewport width), vh (viewport height), vmin (relative to width or height, whichever is smaller), vmax (relative to width or height, whichever is larger) length Units.

For browser compatibility you can check this out.

For a simple tutorial check this out.

Upvotes: 0

Etienne Martin
Etienne Martin

Reputation: 11599

Here's a CSS-only example based on the8472's answer:

div,textarea{
    width:100%;
    max-width: 100%;
    box-sizing: border-box;
    -moz-box-sizing: border-box;
    font-family:  "Lucida Console", Monaco, monospace;
    font-size: 1.99vw; /* The text overflows in safari using (int)2 */
    white-space: nowrap;
    overflow-x:hidden;

    /* styling */ 
    background:#09f;
    min-height: 100px;
    margin-bottom: 30px;
}

How does it work?

To get the right font-size, divide 160 by the number of characters you want to fit per line, minus 0.01.

http://jsfiddle.net/mb2L4mee/1/

Tested in Chrome, FF and safari.

Upvotes: 4

Tob&#237;as
Tob&#237;as

Reputation: 6297

One approach would be to calculate the size of a 80 char line for a fixed font-size and register a function on resize event to adjust the font-size based on the terminal size.

<div id="terminal">
    <div class="text">
        Some text
    </div>
</div>

function scale(lineWith) { /* lineWidth based on 80char 10px font-size */
    var ratio = $('#terminal').width() / lineWith;
    $('.text').css('font-size', (10 * ratio) + 'px');
}
$(window).resize(function(){
    scale(lineWith)
});

Demo: http://jsfiddle.net/44x56zsu/

This seems to adjust pretty good in Chrome (Versión 39.0.2171.99 m)

But I don't think it's possible to adjust the font-size to exactly fit the container size for all browsers because decimal values are rounded. Font size test: http://jsfiddle.net/ahaq49t1/

Font size test result

This rounding issue in some browsers prevents to do an exact adjustment using font properties.

Upvotes: 2

flup
flup

Reputation: 27104

Not sure if this is the most elegant solution, and it needs a bit more polishing too. But you can measure the font. Create a hidden measure bar to see how wide the font is in pixels. Fill it with the desired amount of characters (using 20 here for demonstration purposes):

<span id="measureBar">12345678901234567890</span>

Keep increasing the font size of the measure bar and find the largest width that's still not wider than the div:

$(
  function() {
    var $measureBar = $('#measureBar');
    var $console = $('#console');
    $measureBar.css('font-size', '1px');
    for (i = 1; $measureBar.width() <= $console.width(); i++) {
      $measureBar.css('font-size', i + 'px');
    }
    $console.css('font-size', (i - 1) + 'px');
  }
)
#console {
  background-color: black;
  color: white;
}
#measureBar {
  display: none;
}
#measureBar,
#console {
  font-family: courier;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<span id="measureBar">12345678901234567890</span>
<div id="console">12345678901234567890 Text that should be twenty characters wide.</div>

Upvotes: 1

the8472
the8472

Reputation: 43052

Within limits it might be possible in pure CSS. If you know the fraction of the viewport that the div will occupy then you can scale the font based on the view port width.

.terminal {font-size: 1vw;} would result in a font size (vertical) equivalent to 1% of the view port. Since a monospace font also has a fixed aspect ratio that results in a width depending on the aspect ratio of the font and the view port size.

Upvotes: 1

Related Questions