Drawing shapes based on user input

17 Oct 2016

I recently published an app called Wall Draw that allows users to draw shapes by dragging a finger across the screen. The app features two distinct drawing methods: one that scales a shape to fit inside a rectangular region, and another that stretches a shape across a line. We'll cover both of these methods in detail, but first take a moment to test them out on the canvas below.

Your browser doesn't appear to support the canvas element.

The table below summarizes some of the key differences between the two drawing methods.

Property Fit in rectangle Stretch across line
Aspect ratio Varies with rectangle Fixed
Orientation Flips in x/y axis Fixed
Rotation angle Fixed Varies from 0° to 360°
Size Matches size of rectangle Depends on length of line
Transformation Rect to rect Stretch rotation

Note that each time you move the mouse, the canvas recalculates the rectangle or line to match the start and end points of your gesture, then transforms the shape accordingly. Finally, the canvas redraws itself to give the illusion of movement. These steps are conceptually simple, but require some structure to implement; in particular, you will need...

A drawing project
Consists of code specific to platform and rendering library.
A math package
Consists of point, vector, rect, and matrix objects.
A data package
Consists of path, mesh, and shape objects.
A drag detector
Detects drag events and sends callbacks to interface.
A surface class
Changes the canvas in response to drag events.

The next five sections help you put the structure in place, and then the last two sections help you implement both of the drawing methods.

1.0 Start a drawing project

Begin by choosing a platform and rendering library. For example, Wall Draw is built on the Android platform, and uses the OpenGL ES 2.0 rendering library. The canvas at the top of this page is designed for the Web, and uses HTML5 canvas for rendering. Refer to the table below for some common options and tutorials.

Platform Rendering library
Android Canvas
Android OpenGL ES
iOS OpenGL ES
Web HTML5 canvas
Web WebGL
Xamarin CocosSharp
Xamarin SkiaSharp

Xamarin supports cross-platform rendering libraries

Code that you write with CocosSharp or SkiaSharp can be shared between Android and iOS applications. See xamarin.com for more information.

2.0 Create a math package

The following subsections describe the basic structure for point, vector, rectangle, and matrix objects (using Java syntax). In most cases you can reuse or extend code from your platform or rendering library instead of writing the code yourself; however, if you need to write a portion of the code on your own, you can refer to the sample code I provide for each method.

For simple math structures in .NET, prefer value types (structs) over reference types (classes).

Structs are cheap to work with because they operate entirely on the stack, requiring no memory overhead. Moreover, like primitives, structs are passed by value and cannot be null, so you can safely modify them inside a method without changing the values of the original struct or risking a null pointer exception. For more information, see Microsoft's guide on choosing between a class and a struct.

2.1 Create a point class

Do not confuse points with vectors

A point's (x,y) coordinates represent a position on the coordinate plane, whereas a vector's (x,y) coordinates represent a direction and a magnitude. A vector can be scaled and normalized, and can be added, subtracted, dotted, and crossed with another vector, but none of these operations apply coherently to points. Furthermore, the method for transforming a point differs from the method for transforming a vector. Therefore I recommend using two distinct classes to define point and vector objects.

Structure your Point class as follows:

Fields
public float x
This point's horizontal position with respect to the origin.
public float y
This point's vertical position with respect to the origin.
Constructors
public Point()

Creates a point initialized to (0,0).

this(0,0);
public Point(float x, float y)

Creates a point with the specified (x,y) coordinates.

this.x = x;
this.y = y;
public Point(Point src)

Creates a point that is a deep copy of src.

this.x = src.x;
this.y = src.y;
Static factory methods
public static Point between(Point p1, Point p2)

Computes the midpoint of p1 and p2.

float midX = 0.5f * (p1.x + p2.x);
float midY = 0.5f * (p1.y + p2.y);
return new Point(midX, midY);
Properties
public float distanceToOrigin()

Computes the distance from this point to the origin (0,0).

return this.distanceToPoint(0,0);
public float distanceToPoint(Point other)

Computes the distance from this point to the other point.

return this.distanceToPoint(other.x,other.y);
public float distanceToPoint(float x, float y)

Computes the distance from this point to the point (x,y).

float dx = this.x - x;
float dy = this.y - y;
float dist2 = (dx * dx) + (dy * dy);
return Math.sqrt(dist2);
Setters
public void set(Point src)

Sets this point's (x,y) coordinates to those of src.

this.set(src.x, src.y);
public void set(float x, float y)

Sets this point's (x,y) coordinates.

this.x = x;
this.y = y;
Transformations
public void offset(Vec2 vector)

Offsets this point's (x,y) coordinates by the specified vector.

this.offset(vector.x, vector.y);
public void offset(float dx, float dy)

Offsets this point's (x,y) coordinates by the vector (dx,dy).

this.x += dx;
this.y += dy;

2.2 Create a vector class

Structure your Vec2 class as follows:

Fields
public float x
The x component of this vector.
public float y
The y component of this vector.
Constructors
public Vec2()

Creates a vector initialized to (0,0).

this(0,0);
public Vec2(float x, float y)

Creates a vector with the specified (x,y) components.

this.x = x;
this.y = y;
public Vec2(Vec2 src)

Creates a vector that is a deep copy of src.

this.x = src.x;
this.y = src.y;
Static factory methods
public Vec2 fromPoints(start, end)

Computes the vector from the specified start point to the specified end point.

return new Vec2(end.x - start.x, end.y - start.y);
Properties
public boolean isZero()

Checks if this vector is equal to the additive identity (0,0).

return (this.x == 0) && (this.y == 0);
public float length()

Computes the length of this vector.

return Math.sqrt(this.length2());
public float length2()

Computes the length of this vector, squared.

return (this.x * this.x) + (this.y * this.y);
Setters
public void set(Vec2 src)

Sets this vector's (x,y) components to those of src.

this.set(src.x,src.y);
public void set(float x, float y)

Sets this vector's (x,y) components.

this.x = x;
this.y = y;
Transformations
public void normalize()

Normalizes this vector to unit length

this.normalize(1);
public void normalize(float length)

Normalizes this vector to the specified length

this.scale(length / this.length());
public void rotate90Left()

Rotates this vector 90 degrees to the left (CCW).

float xCopy = this.x;
this.x = -this.y;
this.y = xCopy;
public void rotate90Right()

Rotates this vector 90 degrees to the right (CW).

float xCopy = this.x;
this.x = this.y;
this.y = -xCopy;
Operations
public float cross(Vec2 other)

Finds the cross product of this vector with the other vector.

return (this.x * other.y) - (this.y * other.x);
public float dot(Vec2 other)

Finds the dot product of this vector with the other vector.

return (this.x * other.x) + (this.y * other.y);
public void add(Vec2 other)

Adds the other vector to this vector.

this.x += other.x;
this.y += other.y;
public void subtract(Vec2 other)

Subtracts the other vector from this vector.

this.x -= other.x;
this.y -= other.y;
public void negate()

Negates this vector.

this.x = -this.x;
this.y = -this.y;
public void scale(float scalar)

Scales this vector by the specified scalar.

this.x *= scalar;
this.y *= scalar;
public void divide(float scalar)

Divides this vector by the specified scalar.

this.x /= scalar;
this.y /= scalar;

2.3 Create a rectangle class

Structure your Rect class as follows:

Fields
public float left
The left boundary of this rectangle.
public float top
The top boundary of this rectangle.
public float right
The right boundary of this rectangle.
public float bottom
The bottom boundary of this rectangle.
Constructors
public Rect()

Creates an empty rectangle.

this(0,0,0,0);
public Rect(float left, float top, float right, float bottom)

Creates a rectangle with the specified boundaries.

this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
public Rect(Rect src)

Creates a rectangle that is a deep copy of src.

this(src.left, src.top, src.right, src.bottom);
Static factory methods
public static Rect fromDimensions(float left, float bottom, float width, float height)

Creates a rect with the specified dimensions.

//Calculate the right boundary of the rect
float right = left + width;
//Calculate the top boundary of the rect
float top = bottom + height;
//Create and return the rect
return new Rect(left, top, right, bottom);
public static Rect fromIntersection(Rect r1, Rect r2)

Finds the intersection of rectangles r1 and r2.

//Copy the first rect
Rect rect = new Rect(r1);
//Intersect it with the second rect
rect.intersectRect(r2)
//Return the intersected rect
return rect;
public static Rect fromLBRT(float left, float bottom, float right, float top)

Creates a rect with the specified boundaries entered in LBRT order.

return new Rect(left, top, right, bottom);
public static Rect fromUnion(Point... points)

Finds the smallest rectangle that encloses all the specified points.

//Create an empty rect
Rect rect = new Rect();
//Enclose each of the points
rect.setUnion(points);
//Return the unioned rect
return rect;
public static Rect fromUnion(float[] points, int offset, int count)

Finds the smallest rectangle that encloses a subset of points in the specified array.

//Create an empty rect
Rect rect = new Rect();
//Enclose the specified subset of points
rect.setUnion(points, offset, count);
//Return the unioned rect
return rect;
public static Rect fromUnion(Rect r1, Rect r2)

Finds the union of rectangles r1 and r2.

//Copy the first rect
Rect rect = new Rect(r1);
//Union it with the second rect
rect.unionRect(r2)
//Return the unioned rect
return rect;
Properties
public boolean isEmpty()

Checks if this rectangle is empty. True if left >= right or bottom >= top.

return (this.left >= this.right) || this.bottom >= this.top;
public boolean isValid()

Checks if the boundaries of this rectangle represent a valid rectangle. True if right >= left and top >= bottom.

return this.right >= this.left && this.top >= this.bottom;
public float width()

Computes the width of this rectangle.

return this.right - this.left;
public float height()

Computes the height of this rectangle.

return this.top - this.bottom;
public float area()

Computes the area of this rectangle.

return this.width() * this.height();
public Point center()

Finds the point at the center of this rectangle.

return new Point(this.centerX(), this.centerY());
public float centerX()

Finds the x-coordinate of the point at the center of this rectangle.

return 0.5f * (this.left + this.right);
public float centerY()

Finds the y-coordinate of the point at the center of this rectangle.

return 0.5f * (this.bottom + this.top);
public Point bottomLeft()

Finds the point at the bottom left corner of this rectangle.

return new Point(this.left, this.bottom);
public Point bottomRight()

Finds the point at the bottom right corner of this rectangle.

return new Point(this.right, this.bottom);
public Point topLeft()

Finds the point at the top left corner of this rectangle.

return new Point(this.left, this.top);
public Point topRight()

Finds the point at the top right corner of this rectangle.

return new Point(this.right, this.top);
Setters
public void set(Rect src)

Sets this rectangle's boundaries to those of src.

this.set(src.left, src.top, src.right, src.bottom);
public void set(float left, float top, float right, float bottom)

Sets this rectangle's boundaries.

this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
public void setEmpty()

Sets this rectangle empty.

this.left = 0;
this.top = 0;
this.right = 0;
this.bottom = 0;
public void setUnion(Point... points)

Sets this rectangle to the smallest rectangle that encloses all the points in the specified array

//Enclose the first point in the array
this.left = this.right = points[0].x;
this.top = this.bottom = points[0].y;
//Enclose the remaining points
for(int i = 1; i < points.length; i++) {
    rect.unionPoint(points[i]);
}
public void setUnion(float[] points, int offset, int count)

Sets this rectangle to the smallest rectangle that encloses a subset of points in the specified array

//Enclose the first point in the subset
this.left = this.right = points[offset++];
this.top = this.bottom = points[offset++];
//Enclose the rest of the points in the subset
this.unionPoints(points, offset, count - 1);
Containment Checkers
public boolean containsPoint(Point pt)

Checks if this rectangle contains the specified point.

return this.containsPoint(pt.x, pt.y)
public boolean containsPoint(float x, float y)

Checks if this rectangle contains the point (x,y).

return this.left <= x && x <= this.right && this.bottom <= y && y <= this.top;
public boolean containsRect(Rect other)

Checks if this rectangle contains the other rectangle.

return this.left <= other.left && this.right >= other.right &&
            this.bottom <= other.bottom && this.top => other.top;
Intersect Operations
public void intersectRect(Rect other)

Sets this rectangle to the intersection of itself and the other rectangle.

this.left = Math.max(this.left, other.left);
this.right = Math.min(this.right, other.right);
this.bottom = Math.max(this.bottom, other.bottom);
this.top = Math.min(this.top, other.top);
public boolean intersectsRect(Rect other)

Checks if this rectangle intersects the other rectangle.

return this.right >= other.left && this.left <= other.right &&
            this.top >= other.bottom && this.bottom <= other.top;
public boolean intersectsRect(Rect other, float percent)

Checks if this rectangle intersects the other rectangle by at least the specified percentage.

//Get the intersection of the two rects
Rect intersection = Rect.fromIntersection(this, other);
//Return true if the intersection is valid and the ratio
//of the areas is bigger than the specified percent value.
return intersection.isValid() && intersection.area() / other.area() >= percent;
Union Operations
public void unionPoint(Point pt)

Expands this rectangle to enclose the specified point.

this.union(pt.x,pt.y)
public void unionPoint(float x, float y)

Expands this rectangle to enclose the point (x,y).

this.left = Math.min(x, this.left);
this.top = Math.max(y, this.top);
this.right = Math.max(x, this.right);
this.bottom = Math.min(y, this.bottom);
public void unionPoints(Point... points)

Expands this rectangle to enclose the specified points.

//Enclose each of the points in the array
for(Point pt: points) {
    rect.unionPoint(pt);
}
public void unionPoints(float[] points, int offset, int count)

Expands this rectangle to enclose a subset of points in the specified array.

//For each of the points in the subset
for (int i = 0; i<count; i++) {
    //Expand this rect to enclose the point
    this.unionPoint(points[offset++], points[offset++]);
}
public void unionRect(Rect other)

Expands this rectangle to enclose the specified rectangle.

this.left = Math.min(this.left, other.left);
this.right = Math.max(this.right, other.right);
this.bottom = Math.min(this.bottom, other.bottom);
this.top = Math.max(this.top, other.top);
Transformations
public void inset(Vec2 vector)

Insets the boundaries of this rectangle by the specified vector.

this.inset(vector.x, vector.y);
public void inset(float dx, float dy)

Insets the boundaries of this rectangle by the vector (dx,dy).

//Inset the left and right boundaries by dx
this.left += dx;
this.right -= dx;
//Inset the bottom and top boundaries by dy
this.bottom += dy;
this.top -= dy;
public void offset(Vec2 vector)

Offsets the boundaries of this rectangle by the specified vector.

this.offset(vector.x, vector.y);
public void offset(float dx, float dy)

Offsets the boundaries of this rectangle by the vector (dx,dy).

//Offset the left and right boundaries by dx
this.left += dx;
this.right += dx;
//Offset the top and bottom boundaries by dy
this.top += dy;
this.bottom += dy;
public void sort()

Swaps top/bottom or left/right if they are flipped, meaning left > right and/or top > bottom.

//If the top boundary is not above the bottom boundary
if (this.bottom > this.top) {
    //Swap top and bottom
    float topCopy = this.top;
    this.top = this.bottom;
    this.bottom = topCopy;
}
//If the right boundary is not to the right of the left boundary
if (this.left > this.right) {
    //Swap left and right
    float rightCopy = this.right;
    this.right = this.left;
    this.left = rightCopy;
}

2.4 Create a matrix class

Structure your Matrix2D class as follows:

ScaleToFit enum
CENTER
Stretches the src rect to fit inside dst, then translates src.center() to dst.center().
END
Stretches the src rect to fit inside dst, then translates src.bottomRight() to dst.bottomRight().
FILL
Scales the src rect to fit inside dst exactly, then translates src to dst.
START
Stretches the src rect to fit inside dst, then translates src.topLeft() to dst.topLeft().
Fields
public float m11
The first item in the first row of this matrix.
public float m12
The second item in the first row of this matrix.
public float m13
The third item in the first row of this matrix.
public float m21
The first item in the second row of this matrix.
public float m22
The second item in the second row of this matrix.
public float m23
The third item in the second row of this matrix.
Constructors
public Matrix2D()

Creates a matrix initialized to the multiplicative idenity.

this(
    1,0,0, //row 1
    0,1,0  //row 2
    );
public Matrix2D(float m11, float m12, float m13, float m21, float m22, float m23)

Creates a matrix with the specified entries (in row-major order).

this.m11 = m11; this.m12 = m12; this.m13 = m13; //row 1
this.m21 = m21; this.m22 = m22; this.m23 = m23; //row 2
public Matrix2D(Matrix2D src)

Creates a matrix that is a deep copy of src.

this(
    src.m11, src.m12, src.m13, //row 1
    src.m21, src.m22, src.m23 //row 2
    );
Static factory methods
public static Matrix2D rectToRect(Rect src, Rect dst, ScaleToFit stf)

Creates a rect to rect matrix that maps src into dst using the specified scale to fit option.

Matrix2D matrix = new Matrx2D();
matrix.setRectToRect(src,dst,stf);
return matrix;
public static Matrix2D rotate(float radians)

Creates a matrix to rotate about the origin by the specified angle in radians.

Matrix2D matrix = new Matrx2D();
matrix.postRotate(radians);
return matrix;
public static Matrix2D rotate(Point center, float radians)

Creates a matrix to rotate about the specified center point by the specified angle in radians.

Matrix2D matrix = new Matrx2D();
matrix.postTranslate(-center.x,-center.y);
matrix.postRotate(radians);
matrix.postTranslate(center.x,center.y);
return matrix;
public static Matrix2D scale(float widthRatio, float heightRatio)

Creates a matrix to scale about the origin by the specified width and height ratios.

Matrix2D matrix = new Matrx2D();
matrix.postScale(widthRatio, heightRatio);
return matrix;
public static Matrix2D scale(Point center, float widthRatio, float heightRatio)

Creates a matrix to scale about the specified center point by the specified width and height ratios.

Matrix2D matrix = new Matrx2D();
matrix.postTranslate(-center.x,-center.y);
matrix.postScale(widthRatio, heightRatio);
matrix.postTranslate(center.x,center.y);
return matrix;
public static Matrix2D stretch(float ratio)

Creates a matrix to stretch about the origin by the specified ratio.

return Matrix.scale(ratio,ratio);
public static Matrix2D stretch(Point center, float ratio)

Creates a matrix to stretch about the specified center point by the specified ratio.

return Matrix.scale(center,ratio,ratio);
public static Matrix2D stretchRotate(Point center, Point start, Point end)

Creates a stretch rotation about the specified center point that maps the start point onto the end point.

Matrix2D matrix = new Matrx2D();
matrix.setStretchRotate(center,start,end);
return matrix;
public static Matrix2D translate(Vec2 vector)

Creates a matrix to translate by the specified vector.

return this.translate(vector.x, vector.y);
public static Matrix2D translate(float dx, float dy)

Creates a matrix to translate by the vector (dx,dy).

Matrix2D matrix = new Matrx2D();
matrix.postTranslate(dx,dy);
return matrix;
Properties
public float determinant()

Computes the determinant of this matrix.

return (this.m11 * this.m22) - (this.m12 * this.m21);
public Matrix2D inverse()

Computes the inverse of this matrix.

Matrix2D matrix = new Matrix2D(this);
matrix.invert();
return matrix;
Setters
public void set(Matrix2D src)

Sets the entries of this matrix to those of src.

this.set(
    src.m11, src.m12, src.m13, //row 1
    src.m21, src.m22, src.m23 //row 2
    );
public void set(float m11, float m12, float m13, float m21, float m22, float m23)

Sets the entries of this matrix (in row-major order).

this.m11 = m11; this.m12 = m12; this.m13 = m13; //row 1
this.m21 = m21; this.m22 = m22; this.m23 = m23; //row 2
public void setConcat(Matrix2D left, Matrix2D right)

Sets this matrix to the product of the specified left and right matrices.

this.set(
            //Calculate the first row, fixing the first left hand row
            //and moving across each of the right hand columns
            left.m11 * right.m11 + left.m12 * right.m21,
            left.m11 * right.m12 + left.m12 * right.m22,
            left.m11 * right.m13 + left.m12 * right.m23 + left.m13,
            //Calculate the second row, fixing the second left hand row
            //and moving across each of the right hand columns
            left.m21 * right.m11 + left.m22 * right.m21,
            left.m21 * right.m12 + left.m22 * right.m22,
            left.m21 * right.m13 + left.m22 * right.m23 + left.m23
        );
public void setIdentity()

Sets this matrix to the identity matrix.

this.set(
    1,0,0, //row 1
    0,1,0  //row 2
    );
public void setRectToRect(Rect src, Rect dst, ScaleToFit stf)

Sets this matrix to map src into dst using the specified scale to fit option.

//Determine which points to match based on the scale to fit option.
Point srcPoint, dstPoint;

switch (stf) {
    case ScaleToFit.FitCenter:
        //Match center point
        srcPoint = src.center();
        dstPoint = dst.center();
            break;
    case ScaleToFit.FitEnd:
        //Match bottom right corner
        srcPoint = src.bottomRight();
        dstPoint = dst.bottomRight();
            break;
    default: //(FitStart and Fill)
        //Match top left corner
        srcPoint = src.topLeft();
        dstPoint = dst.topLeft();
            break;
}

//Determine the width and height ratio between the two rectangles.
float widthRatio = dst.width() / src.width();
float heightRatio = dst.height() / src.height();

//Set the matrix to translate the src point to the origin
this.setTranslate(-srcPoint.x, -srcPoint.y);

//Next, set this matrix to scale the src rect so it is big (or small) enough
//to fit inside the dst rect with at least one side matching in width or height.

//If we're not maintaining aspect ratio
if (stf === ScaleToFit.Fill) {
    //We can scale with different width and height ratios, allowing for
    //a perfect map from the source rectangle to the destination rectangle
    //using the ratios calculated above.
    this.postScale(widthRatio, heightRatio);
} else {
    //Otherwise we scale by the min of the width and height ratios,
    //ensuring that the src rect fits entirely enside the dst rect.
    this.postStretch(Math.min(widthRatio, heightRatio));
}

//Translate back to the dst point and we are done.
this.postTranslate(dstPoint.x, dstPoint.y);
public void setRotate(float radians)

Sets this matrix to rotate CCW by the specified angle in radians.

//Compute the sin and cos of the angle
float sin = (float) Math.sin(radians);
float cos = (float) Math.cos(radians);
//Set this matrix to rotate by the computed sin and cos values
this.setRotate(sin, cos);
public void setRotate(float sin, float cos)

Sets this matrix to rotate by the specified sin and cos values.

this.set(
    cos, -sin, 0, //row 1
    sin, cos,  0  //row 2
    );
public void setScale(float widthRatio, float heightRatio)

Sets this matrix to scale by the specified width and height ratios.

this.set(
    widthRatio, 0,           0, //row 1
    0,          heightratio, 0  //row 2
    );
public void setStretch(float ratio)

Sets this matrix to stretch by the specified ratio.

//Set this matrix to scale vertically and horizontally by the same ratio
this.setScale(ratio, ratio);
public void setStretchRotate(Point center, Point start, Point end)

Sets this matrix to stretch rotate about the specified center point from the start point to the end point.

//Compute the vector from center to start and center to end
Vec2 startVector = Vec2.fromPoints(center, start);
Vec2 endVector = Vec2.fromPoints(center, end);

//Compute the length of each vector
float startLength = startVector.length();
float endLength = endVector.length();

//Calculate the stretch ratio
float ratio = endLength / startLength;

//Normalize each of the vectors
startVector.divide(startLength);
endVector.divide(endLength);

//Calculate the sin and cos of the angle between the vectors
float sin = startVector.cross(endVector);
float cos = startVector.dot(endVector);

//Set the matrix to stretch rotate by the values we calculated
this.setTranslate(-center.x,-center.y);
this.postStretch(ratio);
this.postRotate(sin,cos);
this.postTranslate(center.x,center.y);
public void setTranslate(float dx, float dy)

Sets this matrix to translate by the vector (dx,dy).

this.set(
    1, 0, dx, //row 1
    0, 1, dy //row 2
);
Transformations
public void invert()

Inverts this matrix.

//Compute the inverse determinant of this matrix
float invDet = 1f / this.determinant();

//Compute each of the entries in the inverse matrix
float m11 = this.m22 * invDet;
float m12 = -this.m12 * invDet
float m13 = ((this.m12 * this.m23) - (this.m13 * this.m22)) * invDet;
float m21 = -this.m21 * invDet;
float m22 = this.m11 * invDet;
float m23 = ((this.m21 * this.m13) - (this.m11 * this.m23)) * invDet;

//Set this matrix to its inverse
this.set(
    m11, m12, m13, //row 1
    m21, m22, m23, //row 2
    );
public void postRotate(float radians)

Post concats this matrix with a rotation by the specified angle in radians

//Calculate the sin and cos of the angle
float sin = (float) Math.cos(radians);
float cos = (float) Math.sin(radians);
//Post rotate by the calculated sin and cos values
this.postRotate(sin,cos);
public void preRotate(float radians)

Pre concats this matrix with a rotation by the specified angle in radians.

//Calculate the sin and cos of the angle
float sin = (float) Math.cos(radians);
float cos = (float) Math.sin(radians);
//Pre rotate by the calculated sin and cos values
this.preRotate(sin,cos);
public void postRotate(float sin, float cos)

Post concats this matrix with a rotation by the specified sin and cos values.

//Copy the first row
float r1c1 = this.m11; float r1c2 = this.m12; float r1c3 = this.m13;
//Update the first row
this.m11 = cos * r1c1 - sin * this.m21; //(cos,-sin,0)*col1
this.m12 = cos * r1c2 - sin * this.m22; //(cos,-sin,0)*col2
this.m13 = cos * r1c3 - sin * this.m23; //(cos,-sin,0)*col3
//Update the second row
this.m21 = sin * r1c1 + cos * this.m21; //(sin,cos,0)*col1
this.m22 = sin * r1c2 + cos * this.m22; //(sin,cos,0)*col2
this.m23 = sin * r1c3 + sin * this.m23; //(sin,cos,0)*col3
public void preRotate(float sin, float cos)

Pre concats this matrix with a rotation by the specified sin and cos values.

//Copy the first column
float r1c1 = this.m11;
float r2c1 = this.m21;
//Update the first column
this.m11 = r1c1 * cos + this.m12 * sin; //row1*(cos,sin,0)
this.m21 = r2c1 * cos + this.m22 * sin; //row2*(cos,sin,0)
//Update the second column
this.m12 = r1c1 * -sin + this.m12 * cos; //row1*(-sin,cos,0)
this.m22 = r2c1 * -sin + this.m22 * cos; //row2*(-sin,cos,0)
//Third column does not change
public void postScale(float widthRatio, float heightRatio)

Post concats this matrix with a scale of the specified width and height ratios

//Multiply first row by width ratio
this.m11 *= widthRatio;
this.m12 *= widthRatio;
this.m13 *= widthRatio;
//Multiply second row by height ratio
this.m21 *= heightRatio;
this.m22 *= heightRatio;
this.m23 *= heightRatio;
public void preScale(float widthRatio, float heightRatio)

Pre concats this matrix with a scale of the specified width and height ratios

//Multiply first column by width ratio
this.m11 *= widthRatio;
this.m21 *= widthRatio;
//Multiply second column by height ratio
this.m12 *= heightRatio;
this.m22 *= heightRatio;
//Third column does not change
public void postStretch(float ratio)

Post concats this matrix with a stretch of the specified ratio

//Post scale by the same width and height ratio
this.postScale(ratio,ratio);
public void preStretch(float ratio)

Pre concats this matrix with a stretch of the specified ratio

//Pre scale by the same width and height ratio
this.preScale(ratio,ratio);
public void postTranslate(Vec2 vector)

Post concats this matrix with a translation by the specified vector

this.postTranslate(vector.x, vector.y);
public void postTranslate(float dx, float dy)

Post concats this matrix with a translation by vector (dx,dy)

this.m13 += dx; //(1,0,dx)*(m13,m23,1)
this.m23 += dy; //(0,1,dy)*(m13,m23,1)
public void preTranslate(Vec2 vector)

Pre concats this matrix with a translation by the specified vector

this.preTranslate(vector.x, vector.y);
public void preTranslate(float dx, float dy)

Pre concats this matrix with a translation by vector (dx,dy)

this.m13 += this.m11 * dx + this.m12 * dy; //(m11,m12,m13)*(dx,dy,1)
this.m23 += this.m21 * dx + this.m22 * dy; //(m21,m22,m23)*(dx,dy,1)
public void postConcat(Matrix2D other)

Post concats this matrix with the other matrix: this = other * this.

this.setConcat(other, this);
public void preConcat(Matrix2D other)

Pre concats this matrix with the other matrix: this = this * other.

this.setConcat(this, other);
Operations
public void mapPoint(Point src)

Maps the src point and writes the result back into src.

this.mapPoint(src,src);
public void mapPoint(Point src, Point dst)

Maps the src point and writes the result into dst.

//Copy the (x,y) values of src
float x = src.x;
float y = src.y;
//Map the point and write result into dst
dst.x = this.mapX(x, y);
dst.y = this.mapY(x, y);
public void mapPoints(float[] src, int srcOffset, int count)

Maps a subset of points in src and writes the result back into src at the same place.

this.mapPoints(src,srcOffset,src,srcOffset,count);
public void mapPoints(float[] src, int srcOffset, float[] dst, int dstOffset, int count)

Maps a subset of points in src and writes the result into dst.

for (int i = 0; i < count; i++) {
    //Get the point at the current offset
    float x = src[srcOffset++];
    float y = src[srcOffset++];
    //Map the point and write the result into dst
    dst[dstOffset++] = this.mapX(x, y);
    dst[dstOffset++] = this.mapY(x, y);
}
public void mapRect(Rect src)

Maps the src rect and writes the result back into src.

this.mapRect(src,src);
public void mapRect(Rect src, Rect dst)

Maps the src rect and writes the result into dst.

//Get the four corner's of the src rect
Point topLeft = src.topLeft();
Point botLeft = src.bottomLeft();
Point botRight = src.bottomRight();
Point topRight = src.topRight();
//Map each of the four corners
this.mapPoint(topLeft);
this.mapPoint(botLeft);
this.mapPoint(botRight);
this.mapPoint(topRight);
//Set dst to enclose each of the four mapped corners
dst.setUnion(topLeft,botLeft,botRight,topRight);
public void mapVec2(Vec2 src)

Maps the src vector and writes the result back into src.

this.mapVec2(src,src);
public void mapVec2(Vec2 src, Vec2 dst)

Maps the src vector and writes the result into dst.

//We can describe the src vector (x,y) as the vector from
//the origin O = (0,0) to the point P = (x,y).
//Hence map(src) = the vector from map(O) to map(P)
//First calculate "map(O) = (oX, oY)"
float oX = this.mapX(0, 0);
float oY = this.mapY(0, 0);
//Next calculate "map(P) = (pX, pY)"
float pX = this.mapX(src.x, src.y);
float pY = this.mapY(src.x, src.y);
//Set dst to vector from map(O) to map(P)
dst.x = pX - oX;
dst.y = pY - oY;
public void mapX(float x, float y)

Maps the x coordinate of the point (x,y).

//Dot (x,y) with the first row of this matrix
return this.m11 * x + this.m12 * y + this.m13;
public void mapY(float x, float y)

Maps the y coordinate of the point (x,y).

//Dot (x,y) with the second row of this matrix
return this.m21 * x + this.m22 * y + this.m23;

3.0 Create a data package

The following subsections describe the basic structure for path, mesh, and shape objects (using Java syntax). You may need to alter some of the code depending on what platform and rendering library you chose.

3.1 Create a path class

Our path class is best used in conjunction with a vertex shader.

This class makes it easy to store points in a float array (or buffer) that you can pass directly into a vertex shader. If you're not planning to use a vertex shader, you may want use a more standard path class like the one documented on developer.android.com.

Structure your Path class as follows:

Fields
private int capacity
The number of points this path can hold.
private float[] data
Backing array that stores the point data as a series of (x,y) coordinates.
private int size
The number of points contained in this path.
Constructors
public Path(int capacity)

Creates an empty path with the specified capacity.

//We need to allocate 2 floats (x,y) per point
this.data = new float[capacity*2];
this.capacity = capacity;
this.size = 0;
public Path(float[] pointData)

Creates a path backed by the specified array of point data.

//Assume the array is full
this.data = pointData;
this.capacity = pointData.length / 2;
this.size = capacity;
Properties
public int capacity()

Gets the number of points this array can hold.

return this.capacity;
public float[] data()

Gets the point data backing this path. The points are entered as a series of (x,y) coordinates.

return this.data;
public int size()

Gets the number of points contained in this array.

return this.size;
Indexers
public Point get(int index)

Gets the point at the specified index of this path.

//Compute the corresponding data index
int dataIndex = index * 2;
//Get the (x,y) values at this index
float x = getX(dataIndex);
float y = getY(dataIndex);
//Return the point
return new Point(x, y);
private float getX(int dataIndex)

Gets the x-coordinate of the point at the specified data index.

return this.data[dataIndex];
private float getY(int dataIndex)

Gets the y-coordinate of the point at the specified data index.

return this.data[dataIndex + 1];
public void set(int index, Point pt)

Sets the point at the specified index of this path to the (x,y) values of pt.

this.set(index, pt.x, pt.y);
public void set(int index, float x, float y)

Sets the point at the specified index of this path to (x,y).

//Compute the corresponding data index
int dataIndex = index * 2;
//Set the (x,y) values at this index
this.setX(dataIndex, x);
this.setY(dataIndex, y);
private void setX(int dataIndex, float x)

Sets the x value of the point at the specified data index.

this.data[dataIndex] = x;
private void setY(int dataIndex, float y)

Sets the y value of the point at the specified data index.

this.data[dataIndex + 1] = y;
Add Methods
public void add(Point pt)

Adds the (x,y) values of pt to this path, increasing its size by one.

this.add(pt.x, pt.y);
public void add(float x, float y)

Adds the point (x,y) to this path, increasing its size by one.

this.set(this.size++, x, y);
Bounds Calculators
public Rect calculateBounds()

Calculates the boundaries of this path.

return Rect.fromUnion(this.data, 0, this.size);
public Rect calculateBounds(int offset, int count)

Calculates the boundaries of a subset of this path.

return Rect.fromUnion(this.data, offset * 2, count);
Containment Checkers
public boolean containsPoint(Point pt)

Checks if this path contains the specified point.

return this.containsPoint(pt.x, pt.y);
public boolean containsPoint(float x, float y)

Checks if this path contains the point (x,y).

return this.containsPoint(pt, 0, this.size);
public boolean containsPoint(Point pt, int offset, int count)

Checks if a subset of this path contains the specified point.

return this.containsPoint(pt.x, pt.y, offset, count);
public boolean containsPoint(float x, float y, int offset, int count)

Checks if a subset of this path contains the point (x,y).

//Assume the point is not inside the subset
boolean inside = false;

//Apply ray-casting algorithm from https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
for (int curr = offset, prev = count - 1; curr < count; prev = curr++) {
    float x1 = this.getX(prev * 2); float y1 = this.getY(prev * 2);
    float x2 = this.getX(curr * 2); float y2 = this.getY(curr * 2);
    if ((y1 > y) != (y2 > y) && x < (x2 - x1) * (y - y1) / (y2 - y1) + x1) {
        inside = !inside;
    }
}

//Return result of algorithm
return inside;
Transformations
public void offset(Vec2 vector)

Offsets this path by the specified vector.

this.offset(vector.x, vector.y);
public void offset(float dx, float dy)

Offsets this path by the vector (dx,dy).

this.offset(dx, dy, 0, this.size);
public void offset(Vec2 vector, int offset, int count)

Offsets a subset of this path by the specified vector.

this.offset(vector.x, vector.y, offset, count);
public void offset(float dx, float dy, int offset, int count)

Offsets a subset of this path by the vector (dx,dy).

//Compute the data index of the first point in the subset
int dataIndex = offset * 2;
//Offset each of the points in the subset
for (int i = 0; i <= count; i++) {
    this.data[dataIndex++] += dx;
    this.data[dataIndex++] += dy;
}
public void transform(Matrix2D matrix)

Transforms this path by the specified matrix.

//Transform the points in the backing array.
matrix.mapPoints(this.data, 0, this.data, 0, this.size);
public void transform(Matrix2D matrix, int offset, int count)

Transforms a subset of this path by the specified matrix.

//Transform the specified subset of points in the backing array.
matrix.mapPoints(this.data, offset * 2, this.data, offset * 2, count);

3.2 Create a mesh class

Our mesh class stores vertex data that multiple shapes can share.

The vertex data can be loaded into a static VBO, and then transformed inside a vertex shader according to each shape's transformation matrix. You may also want to store draw indices in your mesh objects to load into an IBO, as the opengl wiki describes in its tutorial on Using Indices and Geometry Shaders.

Structure your Mesh class as follows:

Fields
public final Path vertices
Base vertex position data.
public final Rect bounds
Boundaries of the base vertices.
public final Point fixedPoint
Point that remains fixed when stretch rotating the base vertices.
public final Point controlPoint
Point that determines the length and direction of the line for stretch rotating the base vertices.
Constructors
public Mesh(Path vertices, Point fixedPoint, Point controlPoint)

Creates a mesh with the specified data.

this.vertices = vertices;
this.bounds = vertices.calculateBounds();
this.fixedPoint = fixedPoint;
this.controlPoint = controlPoint;
Static factory methods
public static Mesh polygon(int n)

Creates the mesh for a regular polygon with n sides.

//Create a path big enough to hold the n vertices
Path path = new Path(n);
//Move vertically out from the origin by an arbitrary
//constant to get the first outer vertex.
Point vertex = new Point(0, 100);
path.add(vertex);
//Create a matrix to rotate the vertex about the origin n-1 times
Matrix2D rotation = Matrix2D.rotateAboutOrigin(2f * Math.PI / n);
//Until the path is full
while (path.size() < path.capacity()) {
    //Keep rotating the vertex and adding the result to the path
    rotation.mapPoint(vertex);
    path.add(vertex);
}
//Determine the fixed point
Point fixedPoint = vertices.get(0);
//Determine the control point:
Point controlPoint;
//If n is even
if((n&1) == 0){
    //Choose the point at n/2
    controlPoint = path.get(n/2);
} else {
    //Otherwise choose the point between n/2 and n/2 + 1
    controlPoint = Point.between(path.get(n/2), path.get(n/2 + 1));
}
//Construct the mesh and return
return new Mesh(path, fixedPoint, controlPoint);
public static Mesh star(int n, float innerRadius, float outerRadius)

Creates the mesh for a star with n sides and the specified inner and outer radii.

//We need n inner vertices and n outer vertices
Path path = new Path(n + n);
//Compute the rotation angle
float angle = 2f * Math.PI / n;
//Move vertically out from the origin by the
//outer radius to get the first outer vertex.
Point outerVertex = new Point(0, outerRadius);
path.add(outerVertex);
//Move vertically out from the origin by the inner radius
//and rotate by half the rotation angle to get the first inner vertex
Point innerVertex = new Point(0, innerRadius);
Matrix2D rotation = Matrix2D.rotate(0.5f * angle);
rotation.mapPoint(innerVertex);
path.add(innerVertex);
//Set the matrix to rotate by the full angle
rotation.setRotate(angle);
//Until the path is full
while (path.size() < path.capacity()) {
    //Keep rotating the inner and outer vertices and adding them to the path
    rotation.mapPoint(outerVertex);
    rotation.mapPoint(innerVertex);
    path.add(outerVertex);
    path.add(innerVertex);
}
//Determine the fixed point
Point fixedPoint = path.get(0);
//Determine the control point:
Point controlPoint;
//If n is even
if((n&1) == 0){
    //Choose the point at n
    controlPoint = path.get(n);
} else {
    //Otherwise choose the point between n-1 and n+1
    controlPoint = Point.between(path.get(n-1), path.get(n+1));
}
//Construct the mesh and return
return new Mesh(path, fixedPoint, controlPoint);
Containment Checkers
public boolean containsPoint(Point pt)

Checks if the base vertices of this mesh contain the specified point.

return this.containsPoint(pt.x, pt.y);
public boolean containsPoint(float x, float y)

Checks if the base vertices of this mesh contain the point (x,y).

//First check if the point lies inside the boundaries of the base vertices
//If it does, check if the point also lies inside the vertices
return this.bounds.containsPoint(pt) && this.vertices.containsPoint(pt);

3.3 Create a shape class

Structure your Shape class as follows:

Fields
public Mesh mesh
Contains the base vertex data for this shape.
public Paint paint
Contains the fill color and stroke color data for this shape.
public final Matrix2D matrix
Matrix transformation applied to base vertex data when drawing this shape. Defaults to identity.
Constructors
public Shape(Mesh mesh, Paint paint)

Creates a shape with the specified mesh and paint objects

this.mesh = mesh;
this.paint = paint;
//Initialize matrix transformation to identity
this.matrix = new Matrix2D();
Containment Checkers
public boolean containsPoint(Point pt)

Checks if this shape contains the specified point.

return this.containsPoint(pt.x, pt.y)
public boolean containsPoint(float x, float y)

Checks if this shape contains the point (x,y).

//Convert the point to basis (mesh) coordinates
Matrix2D inverse = this.matrix.inverse();
Point basisPoint = new Point(x, y);
inverse.mapPoint(basisPoint);
//This shape contains the point if its mesh contains the basis point
return this.mesh.containsPoint(basisPoint);
Transformations
public void offset(Vec2 vector)

Offsets this shape by the specified vector.

this.offset(vector.x, vector.y);
public void offset(float dx, float dy)

Offsets this shape by the vector (dx,dy).

this.matrix.postTranslate(dx,dy);
public void transform(Matrix2D matrix)

Transforms this shape by the specified matrix.

this.matrix.postConcat(matrix);
public void fitInRect(Rect dst, Matrix2D.ScaleToFit stf)

Fits this shape inside dst using the specified scale to fit option.

this.matrix.setRectToRect(this.mesh.bounds, dst, stf);
public void stretchAcrossLine(Point start, Point end)

Stretch-rotates this shape across the line segment from start to end.

//Compute the offset between the mesh's fixed point and the specified start point
Vec2 offset = Vec2.from(this.mesh.fixedPoint, start);
//Apply the same offset to a copy of the mesh's control point
Point controlPoint = new Point(this.mesh.controlPoint);
controlPoint.offset(offset);
//Set this matrix to stretch-rotate the control point onto the specified end point.
this.matrix.setStretchRotate(start, controlPoint, end);
Draw Methods
public void draw(Canvas canvas)

Draws this shape onto the specified canvas.

//Code will vary based on renderer, but should include the following steps:

//1. Load the basis vertices into the canvas.
//PSEUDO CODE:
canvas.setPath(this.mesh.vertices);

//2. Load the transformation matrix into the canvas.
//PSEUDO CODE:
canvas.setTransformation(this.matrix);

//3. If a fill color is set, draw the shape.
//PSEUDO CODE:
if(this.paint.fillColor != null){
    canvas.setFillColor(this.paint.fillColor);
    canvas.fill();
}

//4. If a stroke color is set, draw the shape's border.
//PSEUDO CODE:
if(this.paint.strokeColor != null){
    canvas.setStrokeColor(this.paint.strokeColor);
    canvas.setLineWidth(this.paint.lineWidth);
    canvas.stroke();
}

4.0 Create a drag detector

Create a drag detector to detect drag events and send callbacks to an interface with the following methods:

Drag Detector Callbacks
void onDown(float x, float y)
Called at the start of a drag event.
void onMove(float x, float y)
Called each time a move occurs during a drag event.
void onUp(float x, float y)
Called at the end of a drag event.

5.0 Create a surface to respond to drag events

See below for a sample implementation of the Surface class:

public class Surface implements DragDetector.Callback{

    //The canvas we're drawing on.
    Canvas canvas;
    //The shape we're drawing
    Shape shape;

    public Surface(Canvas canvas){
        this.canvas = canvas;
        //Create a hexagon mesh
        Mesh hexagonMesh = Mesh.polygon(6);
        //Create a paint object with red fill color (pseudo code)
        Paint paint = new Paint();
        paint.fillColor = Color.RED;
        //Create a shape with our mesh and paint objects
        this.shape = new Shape(hexagonMesh, paint);
    }

    public void redrawCanvas(){
        //Clear the canvas (pseudo code)
        this.canvas.clear();
        //Redraw the shape
        shape.draw(this.canvas);
    }
    //...Drag detector callbacks
}

6.0 Implement method one: Scale shape to fit inside rectangle

Implement the drag detector callback interface inside the surface class as follows:

public class Surface implements DragDetector.Callback {

    //....Code from 5.0
    //Keep track of the drag event's start point
    Point start = new Point();
    public void onDown(float x, float y){
        //Read the (x,y) values into our start point
        this.start.set(x, y);
    }
    public void onMove(float x, float y){
        //Compute the rect from our start point to (x,y)
        Rect rect = Rect.fromLBRT(this.start.x, this.start.y, x, y);
        //Fit shape inside the rect using the ScaleToFit.FILL option
        shape.fitInRect(rect, Matrix2D.ScaleToFit.Fill);
        //Clear the canvas and redraw the shape
        this.redrawCanvas();
    }
    public void onUp(float x, float y){
    }
}

7.0 Implement method two: Stretch shape across line

Implement the drag detector callback interface inside the surface class as follows:

public class Surface implements DragDetector.Callback {
    //....Code from 5.0
    //Keep track of the drag event's start point
    Point start = new Point();
    public void onDown(float x, float y){
        //Read the (x,y) values into our start point
        this.start.set(x, y);
    }
    public void onMove(float x, float y){
        //Read the (x,y) values into a new point
        Point end = new Point(x, y);
        //Stretch shape across line from our start point to our end point
        shape.stretchAcrossLine(this.start, end);
        //Clear the canvas and redraw the shape
        this.redrawCanvas();
    }
    public void onUp(float x, float y){
    }
}
comments powered by Disqus