Skip to content
Steve Ruiz edited this page Feb 21, 2021 · 3 revisions

The MyPaint Brush Library is independent from the rest of MyPaint and can be used from other applications. Since April 6, 2014 it has been moved to its own repository named libmypaint.

  • code: C
  • license: BSD-like
  • dependencies: glib (for random numbers (Not any more in git!) as well as some utility functions, macros, and types e.g. gboolean (Only the version in git!))

The library is currently used by MyPaint, Pixelmator, PostworkShop, GIMP, and Krita (disabled by default). It also seems to be used by MyBrushes. There is a javascript port (online demo) written by Yap Cheah Shen.

Using Brushlib

The documentation below describes Brushlib of MyPaint version 1.0.0. The version in git is a bit different to integrate, it is more like an independent C library. Most details further below are still accurate. --maxy 18:29, 2 September 2012 (UTC)

You have to copy the library source code into your project and adapt it slightly. It is only a bunch of headerfiles with inline implementation, nothing to link against.

Currently there are two "weak" dependencies to worry about:

  • GLib is used for the random generator (and nothing else). It should be trivial to replace it with your own RNG to avoid the dependency.
  • The C Python API is used for the get_state/set_state methods in brush.hpp. You can just delete those two methods, you only need them if you want to replay past strokes.

API

You instantiate the Brush class (brush.hpp) and load the brush settings. Then you call stroke_to(), which will call draw_dab() on your surface:

Loading brush settings

You load your brush settings by calling brush->set_base_value(), brush->set_mapping_n() and brush->set_mapping_point().

Brush settings describe a mapping of inputs (like pressure, velocity, random) to settings (like radius, hardness, color). Most settings will be constant, but almost all of them can be made to depend on the inputs. The settings are described in brushsettings.py (this file is used to generate brushsettings.hpp).

If you want to load a MyPaint brush file (sample file: modelling.myb) you have to parse the file yourself. (Note: recent versions use a different, json-based format!) Code for doing this exists in MyPaint (Python) and in Krita (C++).

Rendering a stroke

You can then make the following call for each motion event:

brush->stroke_to (surface, x, y, pressure, xtilt, ytilt,  dtime)

and the brush will interpolate those events, update its internal states and paint some dabs. Meaning of the parameters:

  • surface is a pointer to your surface that implements draw_dab() (see below)
  • x, y floating point coordinates (many tablets do report sub-pixel coordinates)
  • pressure as reported by tablet
  • xtilt, ytilt are only reported by more expensive tablets, and must be in the range of -1.0 to +1.0.
  • dtime stands for "delta time" and is the number of seconds that have passed between this event and the previous one. You should use an event timestamp if available, not the wallclock time of the method call. Time quality is important to make a good velocity estimate.

You should pass all motion events to brushlib, even if they have zero pressure. If motion events are missing, brushlib will interpolate between them. You can avoid this by calling brush->reset() between strokes, but this is not recommended. Brushlib uses the events between the strokes to estimate the velocity and movement direction, and for some brushes the estimate does average over several seconds. Reseting the brush will lead to glitches in the "speed" and "direction" inputs.

The brush paints dabs by calling surface->draw_dab(). Smudging works by calling surface->get_color(), which returns the color on the surface inside some radius. These two functions are everything that the surface has to implement, from the brush point of view. The abstract interface is in mypaint-surface.h. The parameters passed to those functions might be a bit technical and might change, but you can just implement a subset of them to get started.

Your surface should probably also have a way to be displayed to the user and a way to load from/save to a persistent file format. Possible back-ends for implementing a surface includes Cairo, GEGL or a custom engine. It could theoretically be either vector or raster based.

Dab shape (mask)

The dab shape (also called "mask") is specified by:

  • x, y, radius (all floating point, in pixels)
  • opaque, hardness
  • aspect_ratio and angle (for elliptical dabs)

The parameter opaque (also called dab opacity) is the maximum opacity in the center of the dab. The hardness describes how the opacity fades out with the distance d from the center:

The opacity fades out linear (in two segments) with the squared distance from the center. The result looks like this:

Note: get_color() uses the same mask as draw_dab(). The mask is used to weight the color/alpha of each pixel. However get_color() does not get a hardness as parameter and should use a fixed hardness of 0.5.

For elliptical dabs, the parameters aspect_ratio (range 1.0 to infinity) and angle (range 0 to 180, repeating) are used. In the figure below, aspect_ratio is a/r. Note that the dab can only get smaller than the radius r, which can be used to calculate a bounding box.

Putting it all together results in this pseudocode:

cs = cos(angle/360*2*pi)
sn = sin(angle/360*2*pi)

for each pixel:
  dx = pixel.x - x
  dy = pixel.y - y
  dyr = (dy*cs-dx*sn)*aspect_ratio
  dxr = (dy*sn+dx*cs)
  dd = (dyr*dyr + dxr*dxr) / (radius*radius)
  if dd > 1:
      opa = 0
  else if dd < hardness:
      opa = dd + 1-(dd/hardness)
  else
      opa = hardness/(1-hardness)*(1-dd)
  pixel_opacity = opa * opaque

Use this if you want to stay compatible with MyPaint brushes. If this is not your goal, feel free to render potato-stamped bitmaps instead and completely ignore the hardness! Brushlib won't notice the difference.

Dab blending

Blending is controlled by those parameters:

  • color_r/g/b (RGB color, range 0.0-1.0)

The color of the dab.

  • color_a ("color alpha", used for erasing and smudging)

For normal dabs, this will be 1.0. When set to 0.0 the dab is a pure eraser dab. When set to any other value, the alpha channel of the layer is moved towards color_a, in the same way that the red channel is moved towards color_r on a fully opaque layer. When painting with a pure smudge brush, color_r/g/b/a will be equal to the r/g/b/a picked up from the layer by get_color().

  • lock_alpha

Not usually used by brushes, but it can be set to 1.0 from the GUI to lock the layer's alpha channel.

Brushlib expects the dabs to be put directly to the surface (aka "incremental" mode). There is code to calculate the dab opacity needed to achieve a desired stroke opacity by rendering many overlapping semi-transparent dabs. If you want to use a separate layer with MAX alpha logic instead (also known as "alpha darken"), make sure to set opaque_linearize to zero for your brushes.

MyPaint watercolor/smudge/mix brushes will assume that get_color() can immediately "see" the effect of rendering the previous dab.

Brush inputs and settings

You can find the full list of inputs and settings in brushlib/brushsettings.py and brushsettings.json, including docstrings (which are shown as tooltips in MyPaint).