Assigned: Monday, January 27, 2020,
Due: Wednesday, February 12, 2020 at 1:30pm.

This is the specification for Homework 2 in JavaScript. The specifications for Java are in a different file.

See also the general instructions for homework 2.

If you have questions about this homework, it is best to post them on the class Piazza page, and then I will answer the questions for everyone. Be sure to mention you are doing the homework in JavaScript.

Graphical Objects

You will provide the following graphical object classes:

These classes should be like the following (see GraphicalObject.js):

class OutlineRect extends GraphicalObject {
    constructor(x = 0, y = 0, width = 20, height = 20, color = "black", lineThickness = 1) {}
}

class FilledRect extends GraphicalObject {
    constructor(x = 0, y = 0, width = 20, height = 20, color = "black") { }
}

class Line extends GraphicalObject {
    constructor(x1 = 0, y1 = 0, x2 = 20, y2 = 20, color = "black", lineThickness = 1) {}
}

class Icon extends GraphicalObject {
    constructor(imageFile = undefined, x = 0, y = 0, width=undefined, height=undefined) {}
}

class Text extends GraphicalObject {
    constructor(text = "test", x = 0, y = 0, font = "", color = "black", ctx) {}
}

Your graphical objects should be drawn using the Canvas graphics model as part of an html document. Each object corresponds to a method of the drawing packages: in Canvas, the OutlineRect corresponds to ctx.strokeRect(), etc.

For OutlineRect, FilledRect, and Icon, the (x,y) point is the top-left corner of the of the object (so it is inclusive). For Line, (x1,y1) and (x2,y2) are the endpoints of the line. For Text, (x,y) is the position of the first character's baseline.

Text has the extra parameter ctx for the context, that comes from the Canvas, since this is needed to calculate the size of the string. Note that it should work to call getBoundingBox on a text object before it is displayed.

All of the graphical object classes inherit from GraphicalObject, so you need to implement all of those methods as well:

class GraphicalObject {

    constructor(x = 0, y = 0) { 
        this.x = x;
        this.y = y;
        this.group = null;
    } 
    
    draw(ctx) {}

    /* returns the bounding box as a dictionary with these fields:
     {x: , y: , width:, height: } 
     */
    getBoundingBox() {} 

    moveTo(x, y) {} 

    setGroup(group) { 
        this.group = group; // a real implementation may need to do more than just this
    }

    /* returns a boolean of whether this graphicalObject contains that point or not */
    contains(x, y) {
        return false;
    } 
}

getBoundingBox() returns the smallest rectangle that contains all the pixels drawn by the graphical object. Your graphical objects will have to accurately compute their bounding boxes, taking into account line thickness and the drawing behavior of the graphics package. The bounding box should be the smallest box such that if one were to draw a white filled rectangle using that box, all the pixels would be erased. Or if I set a clip rectangle to that box and drew the object, none of the object would get clipped.

The methods getBoundingBox, contains, and moveTo all use the coordinate system of the parent of the graphical object (the group that this object is in). That is, they are in the same coordinates of the X and Y for a rectangle. It is OK if contains() just checks if the point is inside the bounding box.

Setting the width and height of text or image objects can be defined to be a no-op. Note that the x and y for text objects will be the baseline position, but of course, the x and y in the rectangle returned by getBoundingBox must be for the outside of the text object. However, calling MoveTo on a text object should always move the top left corner to the specified position passed to MoveTo (therefore, mytext.moveTo(10,10) and mytext.y = 10; will not go to the same Y value).

Note that by default in Canvas, drawing a filled rectangle and an outline rectangle using the same values for width and height actually draws different size rectangles, which I think is weird. Please have the width and height parameters of both OutlineRect and FilledRect draw the same size object. In particular, if x is 0 and the width is 3, then pixels 0, 1, and 2 should be drawn by both.

moveTo() moves the graphical object so that the top-left corner of its bounding box is at (x,y). You have to determine how this affects the coordinates of the underlying graphical object. Calling moveTo() on a Line object should move both endpoints so that the line has the same angle and length after the move.

setGroup() sets the group to which the graphical object belongs. If the object doesn't belong to a group, the group value in the object should be null. Groups are described in more detail below.

Drawing

The draw method of each graphical object will draw the appropriate kind of object on the canvas. Note that users of your library do not call draw(), it is only called internally by the toolkit itself. Users of your toolkit will just set properties, and your toolkit will automatically call draw at the appropriate times.The drawing must be clipped by the group, as explained in the next section.

Groups

You will also provide the following grouping classes:

Here are the definitions of those groups:

  class Group extends GraphicalObject {
    constructor(x = 0, y = 0, width = 100, height = 100) { }
    
   /* 
      adds the child to the group. child must be a GraphicalObject.
      Returns false and does nothing if child is in a different group, returns true if successful
    */
    addChild(child) { return false; } 

    /* 
      removes the child from the group. child must be a GraphicalObject.
      Returns false and does nothing if child was not in the group.
      returns true if successfully removed.
    */
    removeChild(child) { return false; }

    /* brings the child to the front. Returns false if child was not in the group, otherwise returns true
    */
    bringChildToFront(child) { return false; }
    
    /* calculates the width and height to just fit all the children, and sets the group to that size.
        returns new [width, height] as an array.
    */
    resizeToChildren() {}

    /* converts the parameter x,y coordinates and returns a point [x, y] array
    */
    parentToChild(x, y) {}

    /* converts the parameter x,y coordinates and returns a point [x, y] array
    */
    childToParent(x, y) {}
}

/* implements the standard Group interface, as defined above. */
class SimpleGroup extends Group {
    constructor(x = 0, y = 0, width = 100, height = 100) {}
}

/* LayoutGroup sets the x,y of each child so they are layed out in a row or column,
depending on the layout parameter. 
There is offset distance between each child object, which can be negative.
*/

const HORIZONTAL = 0;
const VERTICAL = 1;

class LayoutGroup extends Group {
    constructor(x = 0, y = 0, width = 100, height = 100, layout = HORIZONTAL, offset = 10) {}
}

/* ScaledGroup does not change the parameters of its children, but they are all displayed
smaller or bigger depending on the scaleX and scaleY parameters. 
If >1 then bigger, if <1.0 then smaller.
*/
class ScaledGroup extends Group {
    constructor(x = 0, y = 0, width = 100, height = 100, scaleX = 2.0, scaleY = 2.0) {}
}

Notice that the Group interface extends GraphicalObject, so your group classes must implement the GraphicalObject methods too.

All groups have a field called children that is an array of all of the children, or an empty array if no children. This field is not allowed to be set directly - users must call addChild and removeChild. The array is ordered with the back-most child first in the array, so objects are ordered on the screen from the first of the array to the end of the array (the last object in the array is at the front).

bringChildToFront() makes the specified child to be drawn in front of all other children. The default drawing order draws the last child added in front, and the first child added to the group is in the back (so addChild puts the child at the end of the array).

addChild() and removeChild() add and remove graphical objects from the group. These methods should call setGroup() on the child. Objects can be in at most one group at a time - if one attempts to add an object to more than one group, this should raise an exception. removeChild(child) causes that child object to now be in no group.

Each group defines a new coordinate system for its children. The coordinates of the children are interpreted relative to the group's origin (the x, y point passed to the group's constructor). For example, if a top-level group's x,y is 10,20, and a rectangle's x,y is 3,4, then the rectangle will be at 13,24 on canvas.

Each group defines a clipping region for its children, so no child should be drawn outside of its parent group. Another way to say this is that children should be clipped to the bounding box of the group. For example, if a group's x,y,width,height is 10,20,12,13 and a rectangle's x,y is -1,2,5,40, then the rectangle will be clipped on the left and bottom.

resizeToChildren() changes the group's width and height to fit around its children as tightly as possible. The children should not be repositioned, so children at negative (x,y) positions will still lie outside the group's bounding box after this method is called. This is a helper function for the users of your toolkit, and should not be called automatically.

The top-level object, here TopGraphics, can accept one object as its child, which should be some subclass of Group. The only objects that should be visible are ones that are in a group in the parent chain up to TopGraphics. That is, if an object obj1 is added to a group whose parent (or parent's parent, or parent's parent's parent, etc.) is the top-level TopGraphics object, then obj1 will be displayed, but if it is not in a group or if it is removed from its group, then it should not be displayed.

Note that objects should not call draw when properties change -- draw will be called later, often with a collection of objects to be redrawn. (See the discussion in lecture about the tradeoffs of calling draw() on all children vs. calling draw() on only the children that intersect the damaged areas. In this assignment, we are just redrawing all objects.)

When draw() is called on the group, the group should call draw() on all children. Note that it will have to make sure coordinates and clipping are correct for the objects inside the group.

The top-level call to draw() is the redraw call defined in the TopGraphics class. Note that the test programs like TestOutlineRect.js call redraw() after each set of changes when the user should see the changes.

You may want to refer to the lecture and to the required paper by Kosbie, et. al for more details about how drawing and redrawing should work. Note that we are skipping the part about calculating which objects to redraw, and are just redrawing everything each time.

LayoutGroup has two extra parameters that determine how its children are positioned. The layout parameter has the following possible values, defined as constants in the file. HORIZONTAL means that children are placed side by side from left to right, with top edges aligned. VERTICAL means that children are arranged top to bottom, with left edges aligned. The offset parameter specifies how much space to put between each child. Offsets may be negative, in which case adjacent children should overlap. The offset is not used before the first child, which should always be placed at position (0,0). Children should be laid out in drawing order, so bringChildToFront() should make the specified child the last object in the layout.

Extra credit is given for implementing other kinds of automatic layout; see below.

ScaledGroup draws its children scaled in x and y. Scale factors less than 1.0 shrink the children, and scale factors greater than 1.0 enlarge them. Scaling affects both positions and size, so two children of a ScaledGroup with scaleX=0.5 are both half as wide and also half as far apart (in the x direction, at least). Be sure to take scale factors into account when you compute bounding boxes.

Note that groups are transparent, so the parts of a group in which there are no graphical objects should show whatever was there before the group was drawn.

You can access the list of children in a group, but do not modify that list directly - only use addChild and removeChild. The children should be listed in display order, so the last child in the list is the frontmost.

parentToChild() and childToParent() translate between the group's coordinate system and its parent's coordinate system. parentToChild() takes a point in the parent coordinate system and maps it down to the group's coordinate system. For example, if the group is located at (5,10) in its parent, then parentToChild(5,10) should return [0,0]. Similarly, childToParent() maps a point in the group's coordinate system up to the parent coordinate system. Most groups will implement these methods as simple translations, but ScaledGroup should take scaling into account as well.

Other Important Notes

Extra Credit

For extra credit, you can implement more graphical objects, groups, and layouts than the minimum requirement. Here are some ideas. You may have others.

Graphical objects:

Implement a method on GraphicalObject that determines if an (x,y) point hits the object (i.e., is part of the drawn part of the object). That is, gObj.hits(x,y) returns true if and only if x,y is a pixel drawn by gObj. Easy for FilledRects, somewhat or quite tricky for all other kinds of objects.

Layouts:

Resources

The zip file includes the source code for the GraphicalObject and Group definitions, along with the source code for some test programs. These programs include the index.html and other files needed to run them in a browser. Be sure to edit both index.html and index.js to specify which test you want to run. The tests we provide are far from complete, so you should certainly write your own. We will use these test programs and others to grade your project. You can modify the test programs for your own use, but your code shouldn't depend on any changes you make (that is, the original versions of the Test programs should still work).

If you implement additional graphical objects, groups, or layouts, please provide a custom test program for each one that demonstrates its features.

Files:

All the files can be found in this ZIP archive:

hw2-files-js.zip

Remember, there is a Piazza page for this class which may have answers to your questions.
(Last revision of this page: 2/10/2020)

Back to Homework Overview
Back to 05-830 main page