How To ... Develop a Bitmap Painting Tool - Airbrush and Shapes

In this step of the Bitmap Painting tool development, we will add an airbrush option to the tool and a brush shape option to draw both rectangular and circular brushes.

NATURAL LANGUAGE

The existing rollout will be enhanced by adding a dropdown list to define the Brush shape, a checkbutton to enable the AirBrush, and a spinner to control its strength.

We will extend the existing code by drawing random points for the AirBrush option.

The drawStroke function will be enhanced to draw a circle with the Size as the radius.

SCRIPT:

macroScript MicroPaint category: "HowTo"
(
global MicroPaint_CanvasRollout
try (destroyDialog MicroPaint_CanvasRollout) catch()
local isDrawing = false
local bitmapX = bitmapY = 512
local theCanvasBitmap = bitmap bitmapX bitmapY color:white
local currentPos = lastPos = [0,0]

rollout MicroPaint_CanvasRollout "MicroPaint"
(
  bitmap theCanvas pos:[0,0] width:bitmapX height:bitmapY bitmap:theCanvasBitmap
  colorpicker inkColor height:16 modal:false color:black across:4
  checkbutton airBrush "AirBrush" width:50
  spinner AirBrushSpeed "Speed" range:[0.1,50,10] fieldwidth:40
  spinner BrushSize "Size" range:[1,50,10] type:#integer fieldwidth:40
  listbox BrushShape items:#("Circle","Box") pos:[bitmapX+5,0]width:90

  fn paintBrush pos =
  (
    case BrushShape.selection of
    (
      1: (
        if distance pos currentPos <= BrushSize.value/2 do
          setPixels theCanvasBitmap pos #(inkColor.color)
      )
      2:  setPixels theCanvasBitmap pos #(inkColor.color)
     )
  )

  fn drawStroke lastPos pos =
  (
    currentPos = lastPos
    deltaX = pos.x - lastPos.x
    deltaY = pos.y - lastPos.y
    maxSteps = amax #(abs(deltaX),abs(deltaY))
    deltaStepX = deltaX / maxSteps
    deltaStepY = deltaY / maxSteps
    for i = 0 to maxSteps do
    (
      if airBrush.checked then
      (
        for b = 1 to (BrushSize.value / AirBrushSpeed.value) do
        paintBrush (currentPos + (random [-BrushSize.value/2,-BrushSize.value/2] [BrushSize.value/2,BrushSize.value/2] ))
      )
      else
        for b = -BrushSize.value/2 to BrushSize.value/2 do
          for c = -BrushSize.value/2 to BrushSize.value/2 do
            paintBrush (currentPos + [c,b])
      currentPos += [deltaStepX, deltaStepY]
    ) 
    theCanvas.bitmap = theCanvasBitmap
  )

  on MicroPaint_CanvasRollout lbuttondown pos do
  (
    lastPos = pos
    isDrawing = true
    drawStroke lastPos pos
  )
  on MicroPaint_CanvasRollout lbuttonup pos do isDrawing = false
  on MicroPaint_CanvasRollout mousemove pos do
  (
    if isDrawing do drawStroke lastPos pos
    lastPos = pos
  )
)
createDialog MicroPaint_CanvasRollout (bitmapx+100) (bitmapy+30)
)

Step-By-Step

--Code in italic has no changes since the previous version. 
macroScript MicroPaint category:"HowTo"
(
global MicroPaint_CanvasRollout
try(destroyDialog CanvasRollout)catch()
local isDrawing = false
local bitmapX = bitmapY = 512
local theCanvasBitmap = bitmap bitmapX bitmapY color:white
rollout MicroPaint_CanvasRollout "MicroPaint"
(
bitmap theCanvas pos:[0,0] width:bitmapX height:bitmapY bitmap:theCanvasBitmap colorpicker inkColor height:16 modal:false color:black across:4 

To accommodate automatically all new controls in the bottom row, we will have to increase the across: parameter to 4.

checkbutton airBrush "AirBrush" width:50

When checked, this checkbutton will enable the Airbrush feature.

spinner AirBrushSpeed "Speed" range:[0.1,50,10] fieldwidth:40

This value will control the strength of the Airbrush feature.

spinner BrushSize "Size" range:[1,50,10] type:#integer fieldwidth:40 listbox BrushShape items:#("Circle", "Box") width:90 pos:[bitmapx+5,0]

This listbox controls the shape of the brush. In the future versions, we will add additional tools to it.

Listbox

fn paintBrush pos = ( case BrushShape.selection of (

Depending on the setting of the Brush Shape dropdown list, the pixels will be limited inside a circle, or will be drawn as before.

Case Expression

1: (
if distance pos currentPos <= BrushSize.value/2 do
setPixels theCanvasBitmap pos #(inkColor.color)
)

If the Circle shape is selected, the distance from the position to be drawn to the center of the brush will be compared to the radius value (the size divided by 2). Only if the point is at a distance less than the radius, it will be drawn. This defines a circular brush.

2: setPixels theCanvasBitmap pos #(inkColor.color)

This is the original function used in the previous step. It will be used when the dropdown list selection is 2 - "Box".

) ) 
fn drawStroke lastPos pos =
(
currentPos = lastPos
deltaX = pos.x - lastPos.x
deltaY = pos.y - lastPos.y
maxSteps = amax #(abs(deltaX),abs(deltaY))
deltaStepX = deltaX / maxSteps
deltaStepY = deltaY / maxSteps
for i = 0 to maxSteps do
( if airBrush.checked then (

When the Airbrush option is checked, the following code will be executed:

for b = 1 to (BrushSize.value / AirBrushSpeed.value) do

We will repeat a number of times. The actual number will be dependent on the size of the brush and the Airbrush speed value. The higher the Speed value, the lower the number of samples in the brush.

paintBrush (currentPos + (random [-BrushSize.value/2,-BrushSize.value/2] [BrushSize.value/2,BrushSize.value/2] ))

For every iteration, we will draw a random point between the lower left and upper right corners of the box brush. If circular brush is selected, some of the points will be filtered out by the new code in the paintbrush function above.

)
else

Otherwise, the original brush code below will be executed.

for b = -BrushSize.value/2 to BrushSize.value/2 do
for c = -BrushSize.value/2 to BrushSize.value/2 do
paintBrush (currentPos + [c,b])
currentPos += [deltaStepX, deltaStepY]
) 
theCanvas.bitmap = theCanvasBitmap
)
on MicroPaint_CanvasRollout lbuttondown pos do
(
lastPos = pos
isDrawing = true
drawAPoint lastPos pos
)
on MicroPaint_CanvasRollout lbuttonup pos do isDrawing = false
on MicroPaint_CanvasRollout mousemove pos do
(
if isDrawing do drawAPoint lastPos pos
lastPos = pos
) createDialog MicroPaint_CanvasRollout (bitmapX+100)(bitmapY+30)

In order to accommodate the new User Interface controls for the color and brush size, we have to increase the horizontal size of the dialog by 100 pixels.

)

Result

Previous Tutorial:

How To ... Develop a Bitmap Painting Tool - Brush Size and Color

Next Tutorial:

How To ... Develop a Bitmap Painting Tool - Smooth Brushes