Jim Armstrong | ng-conf | Dec 2019
I wanted to something fun for the holiday season, so I decided to port a variable-width stroke from the Flex Freehand Drawing Library I created back in the early 2010’s. This stroke actually has a venerable history, going back to about 1983, as an exercise I was assigned as a teaching assistant for a graduate course in computational geometry. The instructor’s company recently obtained a very expensive tablet. This system allowed users to scan or load drawings already in electronic form into a display and annotate them with hand-drawn notes using a fixed-width stroke. The instructor had an idea for a variable-width (speed-dependent) stroke that would be the basis for a number of lab exercises. My job was to get his idea working in Fortran (yes, now you can laugh at my age). Of course, the Tektronix graphics displays we had at the university did not have the ability to input sequences of pen coordinates, so we had to simulate them with arrays of x- and y-coordinates. Now, you can really laugh at my age!
I breathed some life into this code when it was converted to ActionScript for use in a Flash project and then later formalized into a Flex-based drawing library. It has now been converted to Typescript and packaged into an Angular attribute directive. This directive allows you to imbue a container (primarily a DIV) with freehand drawing ability.
Of course, before we begin, point your friendly, neighborhood browser to this GitHub so that you can obtain the code to use in your own projects.
theAlgorithmist/AngularFreehandDrawing on github.com
Drawing The Stroke
A stroke in general consists of three distinct actions, the first of which is executed on an initial mouse press. The second is executed continually during mouse move. The final action is executed on mouse up.
Actions on mouse-down are largely bookkeeping; record the first mouse press, create an appropriate container in the drawing environment, and initialize all relevant computation variables. The code that accompanies this article draws into a Canvas (using PixiJS). If there is suitable interest, I’ll be glad to publish another article showing how to draw the same stroke into either Canvas or SVG and satisfy the drawing contract at runtime using Angular’s DI system.
Mouse-move actions are a bit more complex. Smoothing is applied to the sequence of mouse coordinates in order to average out some of the ‘shakiness’ in the drawing. An initial width is applied to the stroke, and that width either expands or contracts with mouse speed. The current algorithm increases stroke width with higher mouse velocity, although you could modify the code to enforce the opposite condition. A minimum threshold on stroke width is enforced in the code.
The stroke is divided into ‘endpoints,’ the first end of the stroke and the tip. In between, opposite sides of the stroke are drawn using a sequence of quadratic Bezier curves. Each side of the stroke is essentially a quadratic spline with C-1 continuity, meaning that the spline matches coordinate values and magnitude of first derivative at each join point. The points through which each spline pass are determined by using the direction of the most recently smoothed segment, projected perpendicular in opposite directions based on the variable-width criteria.
Since smoothing is employed and smoothing is a lagging computation, the smoothed stroke computations run behind the current mouse position. The ‘tip’, which extends from most recently smoothed point to current mouse point is drawn with a couple of straight lines and a circle.
So, how does this all work in detail? Well, it’s like … blah, blah, math, blah, blah, API. There, we’re done :).
Now, if you are a seasoned Angular developer, then you are already familiar with attribute directives. Spend five minutes on a high-level review of the demo and you are ready to drop the freehand drawing directive into an application.
If you prefer a more detailed deconstruction and are just starting out with Angular, the remainder of the article discusses how the Typescript code to implement the stroke algorithm is packaged into an Angular attribute directive.
Freehand Drawing Directive
To conserve space, I’ll cover the high points of the directive; review the source code to deconstruct the fine details.
/src/app/drawing/freehand-drawing.directive.ts
The directive selector is ‘freehand’, and the directive can be applied in multiple ways ranging from self-contained interactivity to no internal interactivity. Several parameters may be control by Inputs.
The main app component template, /src/app/app.component.html illustrates several use cases,
<!-- minimal usage
<div class="drawingContainer" freehand></div>
-->
<!-- caching control and begin/end stroke handlers
<div class="drawingContainer" freehand [cache]="cacheStrokes" (beginStroke)="onBeginStroke()" (endStroke)="onEndStroke()"></div>
-->
<!-- control some drawing properties -->
<div class="drawingContainer" freehand [fillColor]="'0xff0000'"></div>
Note that freehand drawing is applied to a container (most likely a DIV) as an attribute. The directive’s constructor obtains a reference to the container and initializes the PixiJS drawing environment. The drawing environment is tightly coupled to the directive in this implementation for convenience.
Since Inputs are defined, the Angular OnChanges interface is implemented. The ngOnChanges method performs light validation of inputs. Mouse handlers are assigned or removed if interactivity is turned on or off.
Caveat: If no Inputs are defined in the HTML container, ngOnChanges is not called. Ensure that all Input values have reasonable defaults.
The OnDestroy interface is also implemented since mouse handlers may be defined. If so, these need to be removed when the directive is is destroyed.
A drawing may contain multiple strokes, so this implementation of the directive stores all containers for each stroke. The coordinates for a single stroke are cached, if desired. This makes it possible to query the x- and y-coordinates for a single stroke.
The directive allows for complete external control. It is possible to load raw mouse coordinates from a server, for example, (i.e. previously stored strokes) and then exercise the API as if the same coordinates were obtained via mouse motion. Previously drawn strokes may be completely redrawn in this manner. It may also be more convenient to control mouse interaction at a higher level than the container. For these reasons, the directive exposes a public API for beginning, updating, and then ending a stroke.
public beginStrokeAt(x: number, y: number, index: number = -1): void
public updateStroke(x: number, y: number):void
public endStrokeAt(x: number, y: number): void
A stroke may also be erased,
public eraseStroke(index: number): boolean
The entire stroke collection may be cleared and the drawing area made available for a new set of strokes,
public clear(): void
The bulk of the work (and the math) is performed in the updateStroke() method. It’s really just some smoothing, analytic geometry, and a couple of quadratic splines with a dynamic tip at the end. As I mentioned at the beginning of the article, don’t credit the drawing algorithm to me; it goes back at least to 1983 to Dr. Tennyson at the University of Texas at Arlington.
On the subject of credit, how about giving yourself some credit for a new dynamic drawing application in Angular? Grab the code, copy and paste, and enjoy some fun holiday coding!
Good luck with your Angular efforts.
ng-conf: Join us for the Reliable Web Summit
Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/
Top comments (0)