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 a SpotFilter
.
-
The
SpotFilter
takes a Plotter
in its constructor. After filtering it calls the plot()
function of the Plotter
.
-
The
Plotter
renders the Spot
on screen. In Markers this is handled by the inner class MarkersPlotter
in Slate
, which draws on the TiledBitmapCanvas
.
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 (called SketchPad
).
-
Make
SketchPad
implement Plotter
, which takes a Spot
and renders to the TiledBitmapCanvas
.
-
Add a
SpotFilter
to SketchPad
. As touch events are captured by onTouch
, pass the Spot
s to the SpotFilter
.
-
In
SketchPad.onDraw()
, call TiledBitmapCanvas.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 = 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!