The shapes

In keeping with the principles that we learned in the previous chapter, we will need a variety of different types to perform different tasks within our application. We will need a representation of what shapes are, how to draw them, something to interact with the user interface, and somewhere to maintain the list of objects to be drawn.

Basic shapes

The first thing we need is a set of classes that will represent the different shapes that we eventually intend on drawing. These objects should be kept separate from the drawing logic that we will implement later. The first thing we need to do is create an abstraction for each of the shapes we intend on representing. This abstraction will allow us to easily extend object types or swap certain objects for others as requirements change. The abstraction is as follows:

interface IPoint {
    x: number;
    y: number;
}
interface IShape {
}
interface IRectangle extends IShape {
    height: number;
    width: number;
    resize(height: number, width: number);
}
interface ICircle extends IShape {
    radius: number;
    resize(radius: number);
    area(): number;
}
interface ILine extends IShape {
    p1: IPoint;
    p2: IPoint;
    length(): number;
}
interface IFreehand extends IShape {
    points: Array<IPoint>;
    addPoint(point: IPoint);
}

As you can see, we have several basic shape interfaces that handle the properties associated with the specific type of shape. The IShape interface is empty for now, but having it provides a common base type for all shapes we create. In the next code sample, the concrete implementation of each of our shape interfaces is shown. These types can be reused across any number of applications that require the representation of shapes and are therefore more useful than if they had been coupled directly to the drawing logic:

class Point implements IPoint {
    constructor(public x: number, public y: number) {
    }
}
class Rectangle implements IRectangle {
    constructor(public height: number, public width: number) {
    }
    public resize(height: number, width: number) {
        this.height = height;
        this.width = width;
    }
}
class Circle implements ICircle {
    constructor(public radius: number) {
    }
    public resize(radius: number) {
        this.radius = radius;
    }
    public area(): number {
        return Math.PI * this.radius * this.radius;
    }
}
class Line implements ILine {
    constructor(public p1: IPoint, public p2: IPoint) {
    }
    public length(): number {
        var a2 = Math.pow(this.p2.x - this.p1.x, 2);
        var b2 = Math.pow(this.p2.y - this.p1.y, 2);
        return Math.sqrt(a2 + b2);
    }
}
class Freehand implements IFreehand {
    public points: Array<IPoint> = [];
    constructor() {
    }
    public addPoint(point: IPoint) {
        this.points.push(point);
    }
}

Each of these classes is limited to a single responsibility that keeps track of the shape's state and reports the various properties associated with it. All of this functionality is useful, but it doesn't get us fully to our goal of having objects drawn on the canvas.

Drawing shapes

Now that we have the basic representation of what we want to draw, we need to define objects that will handle this. These objects should do the least amount of work required to draw the shapes they represent. This means no direct interaction with the Document Object Model (DOM). The following interfaces represent the different abstractions that we will use to represent a shape that can be drawn and manipulated on the canvas element:

interface IDraw {
    draw(ctx: CanvasRenderingContext2D);
}
interface IResize {
    inResizeZone: (mouse: IPoint) => boolean;
    resizeToLocation: (to: IPoint) => void;
}
interface IMove {
    move: (to: IPoint) => void;
    contains: (mousePoint: IPoint, ctx: CanvasRenderingContext2D) => boolean;
    getMoveOffset(mousePos: IPoint): IPoint;
}
interface IDrawingShape extends IDraw, IResize, IMove {
    shape: IShape;
    location: IPoint;
    isSelected: boolean;
    selectionZoneWidth: number;
    opacity: number;    
    getCursorType: (mousePoint: IPoint) => string;
    getClickLocationAction(mouse: IPoint, ctx: CanvasRenderingContext2D): CanvasEngineAction;
}

Each of these interfaces could easily be combined into a single interface, but in accordance with the interface segregation principle, each of these actions can now be decoupled from one another if needs be. The IDrawingShape interface eventually merges each of these types while adding some of its own functionality. All of this put together represents a type that we will be able to draw on the HTML canvas element and interact with in some way. Each IDrawingShape instance holds a reference to its specific shape that can then be accessed or manipulated by our application logic. Separating each shape from the drawing-specific interface upholds the single responsibility principle by allowing the shape to keep track of its data while the drawing objects will only be responsible for methods related to rendering the specific shape in the browser. The shape's location on the canvas is managed by the drawing shape as well as whether it is the currently selected object on the canvas. The getCursorType and getClickLocationAction functions are application-specific and will allow the object responsible for handling user interaction to request information from the drawing objects before proceeding through its application logic. The concrete implementation of this interface is as follows:

class DrawingShapeBase implements IDrawingShape {
    public shape: IShape = null;
    public location: IPoint = new Point(0, 0);
    public isSelected: boolean = false;
    public selectionZoneWidth: number = 4;
    public opacity: number = 1;
    constructor() {
    }
    public inResizeZone(mouse: IPoint): boolean {
        throw "Method not implemented";
    }
    public move(to: IPoint) {
        this.location = to;
    }
    public resizeToLocation(to: IPoint) {
        throw "Method not implemented";
    }
    public contains(mousePoint: IPoint, ctx: CanvasRenderingContext2D): boolean {
        throw "Method not implemented";
    }
    public draw(ctx: CanvasRenderingContext2D) {
        throw "Method not implemented";
    }
    public getMoveOffset(mousePos: IPoint): IPoint {
        return new Point(0, 0);
    }
    public getCursorType(mousePoint: IPoint): string {
        throw "Method not implemented";
    }
    public getClickLocationAction(mousePoint: Point, ctx: CanvasRenderingContext2D): CanvasEngineAction {
        if (this.inResizeZone(mousePoint)) {
            return CanvasEngineAction.Resize;
        }
        else if (this.contains(mousePoint, ctx)) {
            return CanvasEngineAction.Drag;
        }
        return CanvasEngineAction.None;
    }
}

As you can see, this is a very simple base class and is not meant to be instantiated directly. It provides a few default implementations that can be reused by shapes if necessary but are also available to be overloaded. So, now we have our shape abstractions set up, let's start implementing the specific logic for each of the different shapes we intend on drawing in our application. The following screenshot shows a concrete implementation of the DrawingShape type specifically for the Rectangle shape that we defined earlier:

Drawing shapes

We will go over the specific implementations of each of the different methods when we come across their use. The full code is available online. For now, let's just look at the most basic thing we are trying to do, which is to draw a rectangle on the canvas:

public draw(ctx: CanvasRenderingContext2D) {
        ctx.fillStyle = "#FF0000";
        ctx.globalAlpha = this.Opacity;
        ctx.fillRect(this.location.x, this.location.y, this.shape.width, this.shape.height);
        ctx.strokeStyle = "#000000";
        ctx.lineWidth = 3;
        ctx.strokeRect(this.location.x, this.location.y, this.shape.width, this.shape.height);
    }

As we defined earlier in the abstraction of our types, the Draw method takes in one parameter which is of type CanvasRenderingContext2D. This type is a description of the object returned by the canvas element that allows us to draw our different objects directly on the canvas. The rectangle is very easy to draw using the context object's API.

Note

The HTML5 canvas object has a very robust API; for the full list of what it is capable of, visit http://www.w3schools.com/tags/ref_canvas.asp.

The first thing we do is set the color that we want our rectangle to be; this must be done using RGB hex triplets. We can then send the transparency level that the object will be using the globalAlpha property. Next, we use the context to render the rectangle on the canvas object using the properties of the shape object and the drawing object's location. We are also going to display a border around the rectangle, so we set the stroke color in the same fashion, followed by the width of the border that we want to draw. Then, once again, we call upon the context to render the object we want on the canvas. OK, we now have a shape type and a type that can render it in the browser. Let's create a shape and test the application:

var canvas = <HTMLCanvasElement>window.document.getElementById("drawingCanvas");
var ctx = canvas.getContext("2d");
var shape1 = new DrawingRectangle();
shape1.move(new Point(20, 60));
shape1.shape.resize(60, 80);
shape1.draw(ctx);

First we grab the canvas from the DOM and cast it to the HTMLCanvasElement type so that we can easily access its different properties. We need to get the rendering context that will be used by our drawing objects to display their shapes. Then, we create a new DrawingRectangle and move it to a new location and give it a size. Finally, we tell the shape to draw itself using the context object for our canvas element.

In the following screenshot, you can see that we have a red rectangle with a black border around it. It is positioned 20 pixels from the left edge and 60 pixels down from the top edge. The top-left edge of the canvas represents the origin, or Point(0, 0).

Drawing shapes

Now that we can draw the shape on the canvas, let's define how it can be manipulated. The remainder of the work is done by calculations based on the shape type that is being represented and the actions performed by the user. The Move functionality was provided by the base type because it doesn't require any interaction with the shape directly. Any subclass that wants to manipulate how the Move method works can override it, as we learned in Chapter 4, Object-oriented Programming with TypeScript. Next, let's take a look at the methods for resizing DrawingRectangle:

public inResizeZone(point: IPoint): boolean {
    return ((point.X >= this.location.x + this.shape.width - this.selectionZoneWidth &&
        point.x <= this.location.x + this.shape.width + this.selectionZoneWidth) &&
        (point.y >= this.location.y + this.shape.height - this.selectionZoneWidth &&
        point.y <= this.location.y + this.shape.height + this.selectionZoneWidth));
}
public resizeToLocation(to: IPoint) {
    var cursor = window.document.body.style.cursor;
    if (cursor == "se-resize") {
        this.shape.width = to.x - this.location.x;
        this.shape.height = to.y - this.location.y;
    }
}

The first method determines whether the point parameter resides within the boundaries of the selection zone for the drawing object. It will be used to help the engine we build in the next section make decisions. For DrawingRectangle, this is the bottom-right corner; however, hovering over exactly the right pixel to manage this is fairly difficult. This is why there is a selection zone with a configurable size. The ResizeToLocation method takes in a point and modifies the properties of the underlying shape object to resize it. For the rectangle, this is as simple as taking the difference between the incoming point and the current location of the rectangle. The other methods of the abstraction are shown in the following code. They are used to provide information to the canvas engine so that it can appropriately interact with the user.

public contains(mousePoint: IPoint, context: CanvasRenderingContext2D): boolean {
    if (this.shape.height < 0) {
        this.location.y = this.location.y + this.shape.height;
        this.shape.height = this.shape.height * -1;
    }
    if (this.shape.width < 0) {
        this.location.x = this.location.x + this.shape.width;
        this.shape.width = this.shape.width * -1;
    }
    return (this.location.x <= mousePoint.x) &&
        (this.location.x + this.shape.width >= mousePoint.x) &&
        (this.location.y <= mousePoint.y) &&
        (this.location.y + this.shape.height >= mousePoint.y);
}
public getMoveOffset(mousePosition: IPoint): IPoint {
    return new Point(mousePosition.x - this.location.x, mousePosition.y - this.location.y);
}    
public getCursorType(mousePoint: IPoint) {
    if (this.inResizeZone(mousePoint))
        return "se-resize";
    else
        return "move";
}

The contains method does a little bit of maintenance on the state of the shape to ensure it performs the correct calculations. If the height or width of the rectangle is less than zero, then the logic to determine whether the provided point is within the bounds of the shape will not perform as expected. getMoveOffset returns the x and y offsets needed to provide a smooth dragging experience across the canvas. The last piece of the puzzle is the getCursorType method, which tells the engine to inform the user that this region is interactive by changing the cursor.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset