2

I'm trying to recreate this game in JavaScript. For this game, I need cells with numbers in them.

I want the game to size to the available space in the browser. I've managed to do this by using vw and vh in combination with width and min-width (and height) as you can see in the example. If you size the viewport in which the cells are shown, the cells size along.

The problem

And now, I want the text in it to size along too. The container (the cell) resizes, and the font of the digit should size accordingly. I now used vmax as a unit, but this doesn't take the horizontal sizing into account. And since there is no min-font-size, I cannot do the same trick I used for the cells themselves.

No jQuery please

I've tried and searched. Most notably, I found Auto-size dynamic text to fill fixed size container, however I think my question is reversed. The text is fixed, and I could set an initial font-size. I just need the font to scale along with the size of the element, so maybe this can be done through CSS after all.

Besides, most questions about this subject suggest using one of the various jQuery plugins and I'm not looking for a jQuery solution. I'm trying to make this game just for fun and practice, and I've set a goal to create it without jQuery. Actually I'm not even looking for a vanilla JavaScript solution. In the end it may boil down to that, but I haven't tried building it myself yet, so I don't want to ask for JavaScript here now. No, I'm looking for a pure CSS solution, if any.

The snippet

The dressed down snippet works best in full page mode. Nevermind the inline styling. These elements are actually generated by JavaScript and need be moved around. And sorry for the chunk of HTML. I brought it down to two cells at first, but that looked confusing, because they only filled a small part of the screen, and you couldn't see what was going on.

.game11,
.game11 .cell,
.game11 .cell .digit {
  box-sizing: border-box;
}
.game11 {
  width: 90vw;
  height: 90vw;
  max-width: 90vh;
  max-height: 90vh;
  box-sizing: border-box;
  position: relative;
}
.game11 .cell {
  width: 20%;
  height: 20%;
  position: absolute;
  font-size: 7vmax; /* Font size. This obviously doesn't work */
}
.game11 .cell .digit {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  border: 3px solid #666633;
  text-align: center;
  padding-top: 13%;
  font-family: Impact, Charcoal, sans-serif;
  color: #111111;
}
<div class="game11">
  <div class="cell" style="left: 0%; top: 0%;">
    <div class="digit digit2" style="top: 0px;">2</div>
  </div>
  <div class="cell" style="left: 20%; top: 0%;">
    <div class="digit digit2" style="top: 0px;">2</div>
  </div>
  <div class="cell" style="left: 40%; top: 0%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
  <div class="cell" style="left: 60%; top: 0%;">
    <div class="digit digit1" style="top: 0px;">1</div>
  </div>
  <div class="cell" style="left: 80%; top: 0%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
  <div class="cell" style="left: 0%; top: 20%;">
    <div class="digit digit1" style="top: 0px;">1</div>
  </div>
  <div class="cell" style="left: 20%; top: 20%;">
    <div class="digit digit1" style="top: 0px;">1</div>
  </div>
  <div class="cell" style="left: 40%; top: 20%;">
    <div class="digit digit4" style="top: 0px;">4</div>
  </div>
  <div class="cell" style="left: 60%; top: 20%;">
    <div class="digit digit1" style="top: 0px;">1</div>
  </div>
  <div class="cell" style="left: 80%; top: 20%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
  <div class="cell" style="left: 0%; top: 40%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
  <div class="cell" style="left: 20%; top: 40%;">
    <div class="digit digit2" style="top: 0px;">2</div>
  </div>
  <div class="cell" style="left: 40%; top: 40%;">
    <div class="digit digit4" style="top: 0px;">4</div>
  </div>
  <div class="cell" style="left: 60%; top: 40%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
  <div class="cell" style="left: 80%; top: 40%;">
    <div class="digit digit4" style="top: 0px;">4</div>
  </div>
  <div class="cell" style="left: 0%; top: 60%;">
    <div class="digit digit2" style="top: 0px;">2</div>
  </div>
  <div class="cell" style="left: 20%; top: 60%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
  <div class="cell" style="left: 40%; top: 60%;">
    <div class="digit digit5" style="top: 0px;">5</div>
  </div>
  <div class="cell" style="left: 60%; top: 60%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
  <div class="cell" style="left: 80%; top: 60%;">
    <div class="digit digit1" style="top: 0px;">1</div>
  </div>
  <div class="cell" style="left: 0%; top: 80%;">
    <div class="digit digit4">4</div>
  </div>
  <div class="cell" style="left: 20%; top: 80%;">
    <div class="digit digit1" style="top: 0px;">1</div>
  </div>
  <div class="cell" style="left: 40%; top: 80%;">
    <div class="digit digit2" style="top: 0px;">2</div>
  </div>
  <div class="cell" style="left: 60%; top: 80%;">
    <div class="digit digit5">5</div>
  </div>
  <div class="cell" style="left: 80%; top: 80%;">
    <div class="digit digit3" style="top: 0px;">3</div>
  </div>
</div>

Updated: The 'full' (still unfinished) game, including the fix suggested by Pangloss

In the snippet below, you can find the game as I have it so far. It's working for the largest part, so if it's not helpful for the question, at least it may be fun or helpful to future visitors.

/**
 * Game11 class
 */
function Game11(container) {
  var game = this;
  game.element = container;
  game.cells = [];
  game.highestValue = 4;
  game.animations = [];
  game.animating = false;
  var four = this.random(25);
  
  for (var i = 0; i < 25; i++) {
    var cell = new Cell(game, i);
    var value = this.random(3) + 1;
    if (i == four) 
      value = 4;
    cell.setValue(value);
    
    game.cells[i] = cell;
  }
}

Game11.prototype.random = function(to) {
  return Math.floor(Math.random() * to);
}

Game11.prototype.cellClicked = function(cell) {
  if (cell.selected) {
    this.collapse(cell);
  } else {
    this.select(cell);
  }
}

Game11.prototype.collapse = function(cell) {
  var newValue = cell.value + 1;
  if (newValue > this.highestValue) {
    this.highestValue = newValue;
  }
  
  cell.setValue(newValue);
  for (var i = 24; i >= 0; i--) {
    if (this.cells[i].selected) {
      if (i !== cell.index) {
        this.cells[i].setValue(null);
      }
      this.cells[i].select(false);
    }
  }
  for (var i = 24; i >= 0; i--) {
    if (this.cells[i].value == null) {
      this.cells[i].collapse();
    }
  }
  
  this.animate();
}

Game11.prototype.select = function(cell) {
  for (var i = 0; i < 25; i++) {
    this.cells[i].select(false);
  }
  var selectCount = 0;
  var stack = [];
  stack.push(cell);
  while (stack.length > 0) {
    var c = stack.pop();
    c.select(true);
    selectCount++;
    var ac = this.getAdjacentCells(c);
    for (var i = 0; i < ac.length; i++) {
      if (ac[i].selected == false && ac[i].value == cell.value) {
        stack.push(ac[i]);
      }
    }
  }
  if (selectCount == 1)
    cell.select(false);
}

Game11.prototype.getAdjacentCells = function(cell) {
  var result = [];
  if (cell.x > 0) result.push(this.cells[cell.index - 1]);
  if (cell.x < 4) result.push(this.cells[cell.index + 1]);
  if (cell.y > 0) result.push(this.cells[cell.index - 5]);
  if (cell.y < 4) result.push(this.cells[cell.index + 5]);
  return result;
}

Game11.prototype.registerAnimation = function(animation) {
  this.animations.push(animation);
}

Game11.prototype.animate = function() {
  this.animating = true;
  var maxTicks = 300;
  var start = new Date().valueOf();
  var timer = setInterval(function(){
    var tick = new Date().valueOf() - start;
    if (tick >= maxTicks) {
      tick = maxTicks;
      this.animating = false;
    }
    var percentage = 100 / maxTicks * tick;
    
    for (a = 0; a < this.animations.length; a++) {
      this.animations[a].step(percentage);
    }
    
    if (this.animating === false) {
      clearInterval(timer);
      this.animations.length = 0;
      console.log('x');
    }
  }.bind(this), 1);
}

/**
 * A single cell
 */
function Cell(game, index) {
  var cell = this;
  cell.game = game;
  cell.index = index;
  cell.selected = false;
  
  cell.element = document.createElement('div');
  cell.element.className = 'cell';

  cell.digit = document.createElement('div');
  cell.digit.className = 'digit';
  cell.element.appendChild(cell.digit);
  cell.element.addEventListener('click', cell.clicked.bind(cell));

  game.element.appendChild(cell.element);
   
  cell.x = index % 5;
  cell.y = Math.floor((index - cell.x) / 5);
  cell.element.style.left = (cell.x * 20) + '%';
  cell.element.style.top = (cell.y * 20) + '%';
}

Cell.prototype.clicked = function() {
  this.game.cellClicked(this);
}

Cell.prototype.setValue = function(value) {
  this.digit.classList.remove('digit' + this.value);
  this.value = value;
  if (value === null) {
    this.digit.innerText = '';
  } else {
    this.digit.classList.add('digit' + value);
    this.digit.innerText = value;
  }
}

Cell.prototype.select = function(selected) {
  this.element.classList.toggle('selected', selected);
  this.selected = selected;
}

Cell.prototype.collapse = function() {
  var value, y, cellHere, cellAbove;
  var n = this.y;
  var offset = 0;
  do {
    cellHere = this.game.cells[this.x + 5*n];
    y = n - offset;
    value = null;
    do {
      if (--y >= 0) {
        cellAbove = this.game.cells[this.x + 5*y];
        value = cellAbove.value;
        cellAbove.setValue(null);
        if (value !== null) {
          console.log('Value ' + value + ' for cell (' + this.x+','+n+') taken from cell ' + y);
        }
      } else {
        offset++;
        value = this.game.random(Math.max(3, this.game.highestValue - 2)) + 1;
        console.log('New value ' + value + ' for cell (' + this.x+','+n+')');
      }
    } while (value === null);
    
    cellHere.animateDrop(value, n-y);
  } while (--n >= 0)
}

Cell.prototype.animateDrop = function(value, distance) {
  this.setValue(value);
  new Animation(this.game, -distance, this.index, value);
}

/**
 * A cell animation
 */
function Animation(game, from, to, value) {
  this.toCell = game.cells[to];
  var cellBounds = this.toCell.element.getBoundingClientRect();
  var fromX = toX = cellBounds.left;
  var fromY = toY = cellBounds.top;
  if (from < 0) {
    fromY += cellBounds.height * from;
  } else {
    // To do: Moving from one cell to another needs an extra sprite.
    this.fromCell = game.cells[from];
    cellBounds = this.fromCell.element.getBoundingClientRect();
    var fromX = cellBounds.left;
    var fromY = cellBounds.top;
  }
  
  this.fromX = fromX;
  this.fromY = fromY;
  this.toX = toX;
  this.toY = toY;

  this.to = to;
  
  game.registerAnimation(this);
}

Animation.prototype.step = function(percentage) {
  var distance = this.toY - this.fromY;
  var step = (100-percentage) / 100;
  var Y = step * distance;
  this.toCell.digit.style.top = '' + (-Y) + 'px';
}


// Start the game
new Game11(document.querySelector('.game11'));
.game11,
.game11 .cell,
.game11 .cell .digit {
  box-sizing: border-box;
}
.game11 {
  width: 90vmin;
  height: 90vmin;
  box-sizing: border-box;
  position: relative;
  
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.game11 .cell {
  width: 20%;
  height: 20%;
  border: 2px solid #ffffff;
  position: absolute;
  font-size: 10vmin;
}

.game11 .cell .digit {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  border: 3px solid #666633;
  text-align: center;
  padding-top: 13%;
  font-family: Impact, Charcoal, sans-serif;
  color: #111111;
}
.game11 .cell.selected .digit {
  color: white;
}

.game11 .digit.digit1 {
  background-color: #CC66FF;
}
.game11 .digit.digit2 {
  background-color: #FFCC66;
}
.game11 .digit.digit3 {
  background-color: #3366FF;
}
.game11 .digit.digit4 {
  background-color: #99CCFF;
}
.game11 .digit.digit5 {
  background-color: #19D119;
}
.game11 .digit.digit6 {
  background-color: #009999;
}
.game11 .digit.digit7 {
  background-color: #996600;
}
.game11 .digit.digit8 {
  background-color: #009933;
}
.game11 .digit.digit9 {
  background-color: #666699;
}
.game11 .digit.digit10 {
  background-color: #CC66FF;
}
.game11 .digit.digit11,
.game11 .digit.digitmax {
  background-color: #FF0066;
}
<div class="game11">
</div>
Community
  • 1
  • 1
GolezTrol
  • 114,394
  • 18
  • 182
  • 210
  • Can you try `font-size ` with percentage (e.g. `.game11 .cell .digit: { font-size: 50%; }`)? Experiment using different percentage values. – Abraar Arique Diganto Jul 15 '15 at 22:01
  • I should have mentioned I tried that. A percentage makes the font larger or smaller compared to the default (parent) font-size, but it doesn't make it scale with the parent height. – GolezTrol Jul 15 '15 at 22:03
  • Then I would suggest using CSS media queries according to this famous question: http://stackoverflow.com/questions/15649244/responsive-font-size Do you need a customized answer for your situation? – Abraar Arique Diganto Jul 15 '15 at 22:07
  • You mean add a media query for (almost) every screen size? The game resizes along with the window. It's not in big steps of screen larger than 1024px or smaller than 320px. – GolezTrol Jul 15 '15 at 22:12

2 Answers2

2

You could set font size to vmin value.

.game11 .cell {
  font-size: 10vmin;
}

http://jsfiddle.net/a21s77c8/

Stickers
  • 75,527
  • 23
  • 147
  • 186
0

If there is no CSS solution, you can do it by JavaScript. This is quite easy, since all the game's cells are squared and the same size, so you can just get the font size as a factor of the game width. Because of these boundaries, there is no need for a complex library.

All you need to do is remove the font-size from the CSS and add this piece of code to the constructor of Game11:

  // Function that calculates font size based on width of the game itself.
  var updateFontSize = function() {
    var bounds = game.element.getBoundingClientRect(); // Game
    var size = bounds.width / 5; // Cell
    size *= 0.6; // Font a bit smaller
    game.element.style.fontSize = size + 'px';
  };
  // Attach to resize event.
  window.addEventListener('resize', updateFontSize);
  // Initial font size calculation.
  updateFontSize();

Updated game:

/**
 * Game11 class
 */
function Game11(container) {
  var game = this;
  game.element = container;
  game.cells = [];
  game.highestValue = 4;
  game.animations = [];
  game.animating = false;
  var four = this.random(25);
  
  // Function that calculates font size based on width of the game itself.
  var updateFontSize = function() {
    var bounds = game.element.getBoundingClientRect(); // Game
    var size = bounds.width / 5; // Cell
    size *= 0.6; // Font a bit smaller
    game.element.style.fontSize = size + 'px';
  };
  // Attach to resize event.
  window.addEventListener('resize', updateFontSize);
  // Initial font size calculation.
  updateFontSize();
  
  for (var i = 0; i < 25; i++) {
    var cell = new Cell(game, i);
    var value = this.random(3) + 1;
    if (i == four) 
      value = 4;
    cell.setValue(value);
    
    game.cells[i] = cell;
  }
}

Game11.prototype.random = function(to) {
  return Math.floor(Math.random() * to);
}

Game11.prototype.cellClicked = function(cell) {
  if (cell.selected) {
    this.collapse(cell);
  } else {
    this.select(cell);
  }
}

Game11.prototype.collapse = function(cell) {
  var newValue = cell.value + 1;
  if (newValue > this.highestValue) {
    this.highestValue = newValue;
  }
  
  cell.setValue(newValue);
  for (var i = 24; i >= 0; i--) {
    if (this.cells[i].selected) {
      if (i !== cell.index) {
        this.cells[i].setValue(null);
      }
      this.cells[i].select(false);
    }
  }
  for (var i = 24; i >= 0; i--) {
    if (this.cells[i].value == null) {
      this.cells[i].collapse();
    }
  }
  
  this.animate();
}

Game11.prototype.select = function(cell) {
  for (var i = 0; i < 25; i++) {
    this.cells[i].select(false);
  }
  var selectCount = 0;
  var stack = [];
  stack.push(cell);
  while (stack.length > 0) {
    var c = stack.pop();
    c.select(true);
    selectCount++;
    var ac = this.getAdjacentCells(c);
    for (var i = 0; i < ac.length; i++) {
      if (ac[i].selected == false && ac[i].value == cell.value) {
        stack.push(ac[i]);
      }
    }
  }
  if (selectCount == 1)
    cell.select(false);
}

Game11.prototype.getAdjacentCells = function(cell) {
  var result = [];
  if (cell.x > 0) result.push(this.cells[cell.index - 1]);
  if (cell.x < 4) result.push(this.cells[cell.index + 1]);
  if (cell.y > 0) result.push(this.cells[cell.index - 5]);
  if (cell.y < 4) result.push(this.cells[cell.index + 5]);
  return result;
}

Game11.prototype.registerAnimation = function(animation) {
  this.animations.push(animation);
}

Game11.prototype.animate = function() {
  this.animating = true;
  var maxTicks = 300;
  var start = new Date().valueOf();
  var timer = setInterval(function(){
    var tick = new Date().valueOf() - start;
    if (tick >= maxTicks) {
      tick = maxTicks;
      this.animating = false;
    }
    var percentage = 100 / maxTicks * tick;
    
    for (a = 0; a < this.animations.length; a++) {
      this.animations[a].step(percentage);
    }
    
    if (this.animating === false) {
      clearInterval(timer);
      this.animations.length = 0;
      console.log('x');
    }
  }.bind(this), 1);
}

/**
 * A single cell
 */
function Cell(game, index) {
  var cell = this;
  cell.game = game;
  cell.index = index;
  cell.selected = false;
  
  cell.element = document.createElement('div');
  cell.element.className = 'cell';

  cell.digit = document.createElement('div');
  cell.digit.className = 'digit';
  cell.element.appendChild(cell.digit);
  cell.element.addEventListener('click', cell.clicked.bind(cell));

  game.element.appendChild(cell.element);
   
  cell.x = index % 5;
  cell.y = Math.floor((index - cell.x) / 5);
  cell.element.style.left = (cell.x * 20) + '%';
  cell.element.style.top = (cell.y * 20) + '%';
}

Cell.prototype.clicked = function() {
  this.game.cellClicked(this);
}

Cell.prototype.setValue = function(value) {
  this.digit.classList.remove('digit' + this.value);
  this.value = value;
  if (value === null) {
    this.digit.innerText = '';
  } else {
    this.digit.classList.add('digit' + value);
    this.digit.innerText = value;
  }
}

Cell.prototype.select = function(selected) {
  this.element.classList.toggle('selected', selected);
  this.selected = selected;
}

Cell.prototype.collapse = function() {
  var value, y, cellHere, cellAbove;
  var n = this.y;
  var offset = 0;
  do {
    cellHere = this.game.cells[this.x + 5*n];
    y = n - offset;
    value = null;
    do {
      if (--y >= 0) {
        cellAbove = this.game.cells[this.x + 5*y];
        value = cellAbove.value;
        cellAbove.setValue(null);
        if (value !== null) {
          console.log('Value ' + value + ' for cell (' + this.x+','+n+') taken from cell ' + y);
        }
      } else {
        offset++;
        value = this.game.random(Math.max(3, this.game.highestValue - 2)) + 1;
        console.log('New value ' + value + ' for cell (' + this.x+','+n+')');
      }
    } while (value === null);
    
    cellHere.animateDrop(value, n-y);
  } while (--n >= 0)
}

Cell.prototype.animateDrop = function(value, distance) {
  this.setValue(value);
  new Animation(this.game, -distance, this.index, value);
}

/**
 * A cell animation
 */
function Animation(game, from, to, value) {
  this.toCell = game.cells[to];
  var cellBounds = this.toCell.element.getBoundingClientRect();
  var fromX = toX = cellBounds.left;
  var fromY = toY = cellBounds.top;
  if (from < 0) {
    fromY += cellBounds.height * from;
  } else {
    // To do: Moving from one cell to another needs an extra sprite.
    this.fromCell = game.cells[from];
    cellBounds = this.fromCell.element.getBoundingClientRect();
    var fromX = cellBounds.left;
    var fromY = cellBounds.top;
  }
  
  this.fromX = fromX;
  this.fromY = fromY;
  this.toX = toX;
  this.toY = toY;

  this.to = to;
  
  game.registerAnimation(this);
}

Animation.prototype.step = function(percentage) {
  var distance = this.toY - this.fromY;
  var step = (100-percentage) / 100;
  var Y = step * distance;
  this.toCell.digit.style.top = '' + (-Y) + 'px';
}


// Start the game
new Game11(document.querySelector('.game11'));
.game11,
.game11 .cell,
.game11 .cell .digit {
  box-sizing: border-box;
}
.game11 {
  width: 90vw;
  height: 90vw;
  max-width: 90vh;
  max-height: 90vh;
  box-sizing: border-box;
  position: relative;
  
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

.game11 .cell {
  width: 20%;
  height: 20%;
  border: 2px solid #ffffff;
  position: absolute;
}

.game11 .cell .digit {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  border: 3px solid #666633;
  text-align: center;
  padding-top: 13%;
  font-family: Impact, Charcoal, sans-serif;
  color: #111111;
}
.game11 .cell.selected .digit {
  color: white;
}

.game11 .digit.digit1 {
  background-color: #CC66FF;
}
.game11 .digit.digit2 {
  background-color: #FFCC66;
}
.game11 .digit.digit3 {
  background-color: #3366FF;
}
.game11 .digit.digit4 {
  background-color: #99CCFF;
}
.game11 .digit.digit5 {
  background-color: #19D119;
}
.game11 .digit.digit6 {
  background-color: #009999;
}
.game11 .digit.digit7 {
  background-color: #996600;
}
.game11 .digit.digit8 {
  background-color: #009933;
}
.game11 .digit.digit9 {
  background-color: #666699;
}
.game11 .digit.digit10 {
  background-color: #CC66FF;
}
.game11 .digit.digit11,
.game11 .digit.digitmax {
  background-color: #FF0066;
}
<div class="game11">
</div>

Still, if it could be done through CSS, that'd be great.

GolezTrol
  • 114,394
  • 18
  • 182
  • 210