From Spiral03 To Spiral04

The code for the Model Concepts project is at model_concepts on GitHub.

The code for the Magic Boxes project is at mb_spirals on GitHub. For teaching or learning purposes, each spiral in Magic Boxes is decomposed into several sub-spirals.

In Spiral 03, a box may be created by clicking on an empty space in the board, selected and then moved. In Spiral 04, each box has a title and the same four items. A box title has an incremental number and its (x, y) coordinates within the board. A box is moved only if it is selected and if the mouse is pressed down.

The first step towards Spiral 04 is to display a box title with an incremental number (Figure 03-04.01).

Alt Figure 03-04.01: Boxes with title

Figure 03-04.01: Boxes with title.

A box is divided into sections. A smaller section is dedicated to a box title. A box title consists of the Box word and the box number. The first box created has the number 1 in its title. For each new box, this number is incremented by 1.

The title section height is determined by the TBH constant of 20 pixels. The title is displayed with the offset of 4 pixels. The font size is 12. The title number is obtained at the beginning of the constructor from the nextBoxNo property of the board.

Code 03-04.01: Box title preparations.

class Box {
  
  static const int SSS = 6; // selection square size
  static const int TBH = 20; // title box height
  static const int TOS = 4; // text offset size
  
  
  
  String textFontSize = 12;
  String title = "Box";
  int titleNo;
  
  
  
  Box(this.board, this.x, this.y, this.width, this.height) {
    titleNo = board.nextBoxNo;
    draw();
    document.onMouseDown.listen(onMouseDown);
    document.onMouseUp.listen(onMouseUp);
    document.onMouseMove.listen(onMouseMove);
  }
  
}

There is no attribute with the nextBoxNo name in the Board class. However, there is a method with the nextBoxNo name and no parameters, with the get keyword in front of the method name. This method represents a derived property of the Board class that may be referenced as if it was an attribute of the class. The get keyword allows a retrieval of a value that is obtained from the length of the boxes attribute incremented by 1. Without the get keyword, we could not make a simple reference to board.nextBoxNo. Without the get keyword, the next box number could have been obtained by board.nextBoxNo().

Code 03-04.02: The nextBoxNo derived property of the Board class.

int get nextBoxNo() {
    return boxes.length + 1;
  }

After the rectangle of a box is prepared, the line that separates the title from the rest of the box is defined. First, the drawing cursor is moved to the beginning of a line position – the same x, but y is down for the TBH constant. Second, the lineTo method is led to the end of the line position. The font attribute specifies the font of the title. The textAlign attribute determines the horizontal alignment of the text. The textBaseline attribute specifies the vertical alignment of the title. The fillText method prepares the text to be drawn, together with everything else defined between the beginPath and closePath methods, by the stroke method. The fillText method accepts three arguments: the text to display, and the (x,y) coordinates to define where to render it. The fourth argument is optional. It is the maximum width of the text.

Code 03-04.03: The title section drawing.

void draw() {
    board.context.beginPath();
    board.context.rect(x, y, width, height);
    board.context.moveTo(x, y + TBH);
    board.context.lineTo(x + width, y + TBH);
    board.context.font = "bold " + textFontSize + "px sans-serif";
    board.context.textAlign = "start";
    board.context.textBaseline = "top";
    board.context.fillText('${title} ${titleNo}', x + TOS, y + TOS, width - TOS);
    if (isSelected()) {
      board.context.rect(x, y, SSS, SSS);
      board.context.rect(x + width - SSS, y, SSS, SSS);
      board.context.rect(x + width - SSS, y + height - SSS, SSS, SSS);
      board.context.rect(x, y + height - SSS, SSS, SSS);
    } 
    board.context.closePath();
    board.context.stroke();
  }

The property getter is shortened in the Board class from the previous version that is put under the /* … */ comment, so that the whole method becomes one liner. The expression after the => sign is evaluated and its result is returned to a caller of the method.

Code 03-04.04: The shorthand notation for methods.

/*
  int get nextBoxNo() {
    return boxes.length + 1;
  }
  */ 
  int get nextBoxNo() => boxes.length + 1;

The size of a new box is changed from 60 by 100 to 100 by 100 pixels (Code 03-04.05), so that a new title with box coordinates may be displayed within the box (Figure 03-04.02).

Alt Figure 03-04.02: Boxes with coordinates in title

Figure 03-04.02: Boxes with coordinates in title.

If a new box is positioned outside the west and south borders of the board, the box is moved back to the board. This time, the updated x and y coordinates are smaller for 1 pixel in order to see the moved back box in full.

Code 03-04.05: Small changes in the onMouseDown method.

// Create a box in the position of the mouse click on the board, 
  // but not on an existing box.
  void onMouseDown(MouseEvent event) {
    Box box = new Box(this, event.offset.x, event.offset.y, 100, 100);
    bool clickedOnExistingBox = false;
    for (Box box in boxes) {
      if (box.contains(event.offset.x, event.offset.y)) {
        clickedOnExistingBox = true;
        break;
      }
    }
    if (!clickedOnExistingBox) {
      if (event.offset.x + box.width > width) box.x = width - box.width - 1;
      if (event.offset.y + box.height > height) box.y = height - box.height - 1;
      boxes.add(box);
    }
  }

The selected attribute of the Box class has a new name. It is now _selected to indicate that it should not be used directly from outside of the Box class. The private _selected attribute is not initialized to the false value, since Boolean variables are by default false.

Code 03-04.06: The private attributes.

// bool default is false.
  bool _selected;
  bool _mouseDown;

In addition, a new private attribute _mouseDown is defined to keep the mouse down state. A box may be moved only if it is selected and if the mouse is pressed down within the box and kept pressed down while moving the box.

Code 03-04.07: Mouse down state for moving a box.

void onMouseDown(MouseEvent event) {
    if (contains(event.offset.x, event.offset.y)) toggleSelection();
    _mouseDown = true;
  }
  
  void onMouseUp(MouseEvent event) {
    _mouseDown = false;
  }
  
  // Change a position of the box with mouse mouvements.
  void onMouseMove(MouseEvent event) {
    if (isSelected() && contains(event.offset.x, event.offset.y) 
      && _mouseDown) {
      x = event.offset.x - width / 2;
      if (x < 0) x = 1;
      if (x > board.width - width) x = board.width - width - 1;
      y = event.offset.y - height / 2;
      if (y < 0) y = 1;
      if (y > board.height - height) y = board.height - height - 1;
    }
  }

The selection methods from the previous spiral are now condensed into 4 lines of code.

Code 03-04.08: The shorthand notation for selection and string methods.

select() => _selected = true;
  deselect() => _selected = false;
  toggleSelection() => _selected = !_selected;
  bool isSelected() => _selected;
  
  String toString() => '$title$titleNo ($x, $y)';

Similarly, the toString method of the Box class returns the text by combining values of the four variables into a title, which is then used in the fillText method.

Code 03-04.09: The box title with coordinates.

void draw() {
    board.context.beginPath();
    board.context.rect(x, y, width, height);
    board.context.moveTo(x, y + TBH);
    board.context.lineTo(x + width, y + TBH);
    board.context.font = "bold " + textFontSize + "px sans-serif";
    board.context.textAlign = "start";
    board.context.textBaseline = "top";
    board.context.fillText(toString(), x + TOS, y + TOS, width - TOS);
    if (isSelected()) {
      board.context.rect(x, y, SSS, SSS);
      board.context.rect(x + width - SSS, y, SSS, SSS);
      board.context.rect(x + width - SSS, y + height - SSS, SSS, SSS);
      board.context.rect(x, y + height - SSS, SSS, SSS);
    } 
    board.context.closePath();
    board.context.stroke();
  }

The canvas size is increased to 800 by 600 pixels in the mb.html file. This increase allows for more boxes to be drawn on the board.

Code 03-04.10: The increased canvas size.

<canvas id="canvas" width="800" height="600">
        Canvas is not supported in your browser.
      </canvas>

In addition to a title with the sequential number and (x, y) coordinates, each new box has 4 items displayed in the second section of the box. Item names are, for the time being, the same in all boxes (Figure 03-04.03).

Alt Figure 03-04.03: Boxes with items

Figure 03-04.03: Boxes with items.

The new item attribute is initialized to the “Item” value.

Code 03-04.11: Item preparations.

class Box {
  
  static const int SSS = 6; // selection square size
  static const int TBH = 20; // title box height
  static const int TOS = 4; // text offset size
  static const int IOS = TBH - TOS; // item offset size
  
  String item = "Item";

The IOS constant is used to draw the four items properly.

Code 03-04.12: Item drawing.

void draw() {
    board.context.beginPath();
    board.context.rect(x, y, width, height);
    board.context.moveTo(x, y + TBH);
    board.context.lineTo(x + width, y + TBH);
    board.context.font = 'bold ${textFontSize}px sans-serif';
    board.context.textAlign = "start";
    board.context.textBaseline = "top";
    board.context.fillText(toString(), x + TOS, y + TOS, width - TOS);
    board.context.fillText('${item}1', x + TOS, y + TOS + TBH, width - TOS);
    board.context.fillText('${item}2', x + TOS, y + TOS + TBH + IOS, width - TOS);
    board.context.fillText('${item}3', x + TOS, y + TOS + TBH + 2 * IOS, width - TOS);
    board.context.fillText('${item}4', x + TOS, y + TOS + TBH + 3 * IOS, width - TOS);
    if (isSelected()) {
      board.context.rect(x, y, SSS, SSS);
      board.context.rect(x + width - SSS, y, SSS, SSS);
      board.context.rect(x + width - SSS, y + height - SSS, SSS, SSS);
      board.context.rect(x, y + height - SSS, SSS, SSS);
    }
    board.context.closePath();
    board.context.stroke();
  }

When a selected box is moved over another selected box, its movement is stopped to prevent the moving box to become a monster. This is accomplished by using the countSelectedBoxesThatContain method (Code 03-04.13) of the Board class in the onMouseMove method (Code 03-04.14) of the Box class.

Code 03-04.13: Counting selected boxes.

int countSelectedBoxesThatContain(int px, int py) {
    int count = 0;
    for (Box box in boxes) {
      if (box.isSelected() && box.contains(px, py)) count++;
    }
    return count;
  }

The countSelectedBoxesThatContain method counts the number of selected boxes that contain a given point. This point becomes the event offset in the onMouseMove method. If there is no other selected box that contains the same point, the current box is moved freely.

Code 03-04.14: Selected boxes counted.

void onMouseMove(MouseEvent event) {
    int ex = event.offset.x; 
    int ey = event.offset.y;
    if (contains(ex, ey) && isSelected() && _mouseDown 
        && board.countSelectedBoxesThatContain(ex, ey) < 2) {
      x =  ex - width / 2;
      if (x < 0) x = 1;
      if (x > board.width - width) x = board.width - width - 1;
      y = ey - height / 2;
      if (y < 0) y = 1;
      if (y > board.height - height) y = board.height - height - 1;
    }
  }

However, if there is another selected box that contains the same mouse event point, the current box cannot move over the other box in order to prevent that the two boxes overlap each other and move together as one (Figure 03-04.04).

Alt Figure 03-04.04: Two selected boxes cannot overlap each other

Figure 03-04.04: Two selected boxes cannot overlap each other.

blog comments powered by Disqus