Back when I started Monkey Write at the AT&T hackathon, I was already varying the pen stroke width by the pressure of the touch events. But the pressure ranges change a lot on depending on the device. I was not able to get consistent stroke rendering with all those pressure ranges, so I only vary the stroke width by pressure if I know it is from the active stylus reported via an HTC PenEvent.
I was really excited when I saw the beautiful Markers app. Its pressure-sensitive strokes work on many devices, plus it is open sourced with Apache License 2.0, so all I have to do is to integrate that into Monkey Write. Well, all I have to do is to find time to integrate that into Monkey Write, which I finally did!
PressureCooker
When I first looked into the Markers code I was very amused by the PressureCooker
class. What a name! It calibrates the pressure coming from a series of touch events, which is what I need. I lifted that class and put it into Monkey Write, but that alone did not make beautiful strokes. This was where I stopped the first time I looked into Markers, for the rest involves a more complicated co-ordination among the Slate
, TiledBitmapCanvas
and SpotFilter
classes.
Markers architecture
I finally set aside some time to understand how Markers work.
-
Convert each incoming touch event into a
Spot
. -
Add the
Spot
to aSpotFilter
. -
The
SpotFilter
takes aPlotter
in its constructor. After filtering it calls theplot()
function of thePlotter
. -
The
Plotter
renders theSpot
on screen. In Markers this is handled by the inner classMarkersPlotter
inSlate
, which draws on theTiledBitmapCanvas
.
Where is the PressureCooker
, you ask? It is used inside the plot()
function. Instead of using Spot.pressure
directly, it is calibrated by the PressureCooker
.
Monkey Write modifications
Here is what I did to incorporate Markers stroke rendering into Monkey Write:
-
Replace my own class with
Spot
to store touch events. -
Add a
TiledBitmapCanvas
to the character writing custom view (calledSketchPad
). -
Make
SketchPad
implementPlotter
, which takes aSpot
and renders to theTiledBitmapCanvas
. -
Add a
SpotFilter
toSketchPad
. As touch events are captured byonTouch
, pass theSpot
s to theSpotFilter
. -
In
SketchPad.onDraw()
, callTiledBitmapCanvas.drawTo()
after rendering the base character to show the pen strokes. -
After the user writes a stroke, grade it. If it was not a good stroke, call
TiledBitmapCanvas.step(-1)
to remove that stroke.
Pen styles
After I set up the basic pressure cooking and spot filtering I started to experiment with different pen styles. This essentially means changing the pen tip, or how to render each touch point aka Spot
.
Basic style
The basic style renders the pen tip as a solid circle. Fairly straight forward:
c.drawCircle(x, y, r, mPaint);
Brush style
Markers has an airbrush style, which draws a bitmap as the pen tip. I looked at the bitmap and thought, hey, that's just a RadialGradient
! I decided to generate that programmatically so I can vary the alpha value on the fly:
private Shader createBrushShader( float width, float alphaStart, float alphaEnd) { final float center = width / 2; final float radius = Math.max(1, width / 2); final int red = Color.red(mPaint.getColor()); final int green = Color.green(mPaint.getColor()); final int blue = Color.blue(mPaint.getColor()); return new RadialGradient( center, center, radius, Color.argb(alphaStart, red, green, blue), Color.argb(alphaEnd, red, green, blue), Shader.TileMode.CLAMP); }
mPaint.setShader(mBrushShader); c.drawCircle(x, y, r, mPaint);I update
mBrushShader
with createBrushShader()
whenever the user changes the width or alpha from the UI.
Pencil style
Once I started playing with Shader
s I could not stop. I decided to mimic a pencil stroke on a rough paper by plotting little dots at the pen tip.
private Shader createPencilShader(float alpha) { final int size = 32; int color = Color.rgb( Color.red(mPaint.getColor()), Color.green(mPaint.getColor()), Color.blue(mPaint.getColor())); float threshold = alpha / 255f; int[] colors = new int[size * size]; for (int i = 0; i < colors.length; ++i) { colors[i] = (Math.random() >= threshold) ? 0 : color; } Bitmap bitmap = Bitmap.createBitmap( colors, size, size, Bitmap.Config.ARGB_8888); return new BitmapShader( bitmap, TileMode.REPEAT, TileMode.REPEAT); }
mPaint.setShader(mPencilShader); c.drawCircle(x, y, r, mPaint);
I use a BitmapShader
to draw the little dots. It is a tiling bitmap, each pixel is either transparent or the chosen color. The lower the alpha value, the more transparent pixels.
Constant width
Finally I want to provide an option for users who don't want variable width. This is achieved by ignoring the pressure from the touch event and supplying a constant value to the pen tip renderer.
Mix and match
With that you can have a lot of fun making different pen styles.
Here is a very transparent blue stroke. Looks like water, doesn't it?
You can pick different styles for different strokes:
Hopefully these beautiful strokes will make it even more fun to practice writing Chinese. Download Monkey Write and try them out!