How To ... Develop a Bitmap Painting Tool - Erase Changes

How To > Develop a Bitmap Painting Tool- Erase Changes

In this step of the Bitmap Painting tool development, we will add an eraser option to the existing brushes. When the user is drawing with the right mouse button pressed, the background image will be revealed.This mode will support all available brushes including the Airbrush mode, allowing the user to erase smoothly.

NATURAL LANGUAGE

We will add a new flag variable to control the Erase mode

We will change the paintbrush function to support reading pixels from the background image

We will implement right mouse button handlers to enable erasing existing pixels with the original pixels from the background

We will add a commit changes option to the Edit menu top copy the foreground painting canvas into the background and "bake in" all changes.

SCRIPT:

macroScript MicroPaint category: "HowTo"
(
global MicroPaint_CanvasRollout
try (destroyDialog MicroPaint_CanvasRollout) catch()
local isErasing =isDrawing = false
local bitmapX = bitmapY = 512
local theCanvasBitmap = bitmap bitmapX bitmapY color:white
local theBackgroundBitmap = bitmap bitmapX bitmapY color:white
local currentPos = lastPos = [0,0]
 
rcMenu CanvasMenu
(
  subMenu "File"
  (
    menuItem new_menu "New"
    menuItem open_menu "Open..."
    menuItem save_as "Save As..."
    separator file_menu_1
    menuItem quit_tool "Quit"  
  )
  subMenu "Edit"
  (
     menuItem commit_menu "Commit Changes"
  )
  on commit_menu picked do copy theCanvasBitmap theBackgroundBitmap
 
  subMenu "Help"
  (
    menuItem about_tool "About MicroPaint..." 
  )
 
  on new_menu picked do
  (
    theBackgroundBitmap = theCanvasBitmap = bitmap bitmapX bitmapY color:MicroPaint_CanvasRollout.paperColor.color
    MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
  )
 
 
  on open_menu picked do
  (
    theOpenBitmap= selectBitmap()
    if theOpenBitmap != undefined do
    (
      copy theOpenBitmap theCanvasBitmap
      copy theOpenBitmap theBackgroundBitmap
      close theOpenBitmap
      MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
    )
  )
 
  on save_as picked do
  (
    theSaveName = getSaveFileName types:"BMP (*.bmp)|*.bmp|Targa (*.tga)|*.tga|JPEG (*.jpg)|*.jpg"
    if theSaveName != undefined do
    (
      theCanvasBitmap.filename = theSaveName
      save theCanvasBitmap
    )
  )
  on about_tool picked do messagebox "MicroPaint\nMAXScript Tutorial" title:"About..."
  on quit_tool picked do destroyDialog MicroPaint_CanvasRollout
)
 
rollout MicroPaint_CanvasRollout "MicroPaint"
(
  bitmap theCanvas pos:[0,0] width:bitmapX height:bitmapY bitmap:theCanvasBitmap
  colorpicker inkColor height:16 modal:false color:black across:5
  colorpicker paperColor height:16 modal:false color:white
  checkbutton airBrush "AirBrush" width:50
  spinner AirBrushSpeed "Speed" range:[0.1,50,10] fieldwidth:30
  spinner BrushSize "Size" range:[1,50,10] type:#integer fieldwidth:40
  listbox BrushShape items:#("Circle","Box","Circle Smooth") pos:[bitmapX+5,0] width:90
 
  fn paintBrush pos =
  (
    if isErasing then
      thePaintColor = (getPixels theBackgroundBitmap pos 1)[1]
    else
      thePaintColor = inkColor.color
    if thePaintColor == undefined do thePaintColor = white
 
    case BrushShape.selection of
    (
      1: (
        if distance pos currentPos <= BrushSize.value/2 do
          setPixels theCanvasBitmap pos #(thePaintColor)
      )
      2: setPixels theCanvasBitmap pos #(thePaintColor)
      3: (
        theFactor = (distance pos currentPos) / (BrushSize.value/2.0)
        if theFactor <= 1.0 do
        (
          theFactor = sin ( 90.0 * theFactor)
          thePixels = getPixels theCanvasBitmap pos 1
          if thePixels[1] != undefined do
          (
            thePixels[1] = (thePixels[1] * theFactor) + (thePaintColor * (1.0 - theFactor))
              setPixels theCanvasBitmap pos thePixels
          )
        )
      )--end case 3
    )--end case
  )--end fn
 
  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
    isErasing = false
    drawStroke lastPos pos
  )
  on MicroPaint_CanvasRollout rbuttondown pos do
  (
    lastPos = pos
    isErasing = isDrawing = true
    drawStroke lastPos pos
  )
  on MicroPaint_CanvasRollout lbuttonup pos do isErasing =isDrawing = false
  on MicroPaint_CanvasRollout rbuttonup pos do isErasing =isDrawing = false
 
  on MicroPaint_CanvasRollout mousemove pos do
  (
    if isDrawing do drawStroke lastPos pos
    lastPos = pos
  )
)
createDialog MicroPaint_CanvasRollout (bitmapx+100) (bitmapy+30) menu:CanvasMenu
MicroPaint_CanvasRollout.theCanvas.bitmap = theBackgroundBitmap
)

Step-By-Step

--Code in green has not changes since the previous version! macroScript MicroPaint category:"HowTo"
(
global MicroPaint_CanvasRollout
try(destroyDialog MicroPaint_CanvasRollout)catch() local isErasing = isDrawing = false

We add a new variable isErasing which will be set to true when the right mouse button is pressed.

local bitmapX = bitmapY = 512
local theCanvasBitmap = bitmap bitmapX bitmapY color:white
local theBackgroundBitmap = bitmap bitmapX bitmapY color:white
local currentPos = lastPos = [0,0]

rcMenu CanvasMenu
(
subMenu "File"
(
menuItem new_menu "New"
menuItem open_menu "Open..."
menuItem save_as "Save As..."
separator file_menu_1
menuItem quit_tool "Quit"
)
subMenu "Edit"
( menuItem commit_menu "Commit Changes"

We add a new menu item to the Edit dialog. It will be used to commit the editing changes and make them "unerasable".

) on commit_menu picked do copy theCanvasBitmap theBackgroundBitmap

This is the event handler for the commit changes option. When selected, the foreground image we are painting on will be copied into the background image, thus "baking" all the changed pixels into it. Using the eraser from this point on will reveal the new pixels.

subMenu "Help"
(
menuItem about_tool "About MicroPaint..." 
)

on new_menu picked do
(
theBackgroundBitmap = theCanvasBitmap = bitmap bitmapX bitmapY color:MicroPaint_CanvasRollout.paperColor.color
MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
)

on open_menu picked do
(
theOpenBitmap= selectBitmap()
if theOpenBitmap != undefined do
(
copy theOpenBitmap theCanvasBitmap
copy theOpenBitmap theBackgroundBitmap
close theOpenBitmap
MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
)
)

on save_as picked do
(
theSaveName = getSaveFileName types:"BMP (*.bmp)|*.bmp|Targa (*.tga)|*.tga|JPEG (*.jpg)|*.jpg"
if theSaveName != undefined do
(
theCanvasBitmap.filename = theSaveName
save theCanvasBitmap
)
)

on about_tool picked do messagebox "MicroPaint\nMAXScript Tutorial" title:"About..."
on quit_tool picked do destroyDialog MicroPaint_CanvasRollout
)

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

fn paintBrush pos =
( if isErasing then

If the eraser mode is active (that means, the user is holding down the right mouse button), then

thePaintColor = (getPixels theBackgroundBitmap pos 1)[1]

we get the color of the pixel from the background as the ink color.

else
thePaintColor = inkColor.color

otherwise we use the ink color defined by the User Interface.

if thePaintColor == undefined do thePaintColor = white

If the pixel was outside the bitmap, it might contain undefined. To avoid errors, we reset it back to white.

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

In both cases, we replace the explicit use of the InkColor.color with thepaintColor which contains the correct color to paint with.

3: (
theFactor = (distance pos currentPos) / (BrushSize.value/2.0)
if theFactor <= 1.0 do
(
theFactor = sin ( 90.0 * theFactor)
thePixels = getPixels theCanvasBitmap pos 1
if thePixels[1] != undefined do
( thePixels[1] = (thePixels[1] * theFactor) + (thePaintColor * (1.0 - theFactor)) 

Again, we replace the explicit use of the InkColor.color with thePaintColor which contains the correct color to paint with.

setPixels theCanvasBitmap pos thePixels
)
)
)--end case 3
)--end case
)--end fn

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 isErasing = false

The isErasing flag is set to false whenever the user presses the left mouse button.

drawStroke lastPos pos
) on MicroPaint_CanvasRollout rbuttondown pos do
(
lastPos = pos
isErasing = isDrawing = true
drawStroke lastPos pos
)

This new handler is called whenever the right mouse button is pressed. It is identical to the left mouse button handler except for the isErasing flag which is set to true this time.

on MicroPaint_CanvasRollout lbuttonup pos do isErasing = isDrawing = false
on MicroPaint_CanvasRollout rbuttonup pos do isErasing = isDrawing = false

Both the lbuttonup and the new rbuttonup handlers will stop both drawing and erasing by setting both flags to false.

on MicroPaint_CanvasRollout mousemove pos do
(
if isDrawing do drawStroke lastPos pos
lastPos = pos
)
)
createDialog MicroPaint_CanvasRollout (bitmapx+100) (bitmapy+30) menu:CanvasMenu
MicroPaint_CanvasRollout.theCanvas.bitmap = theBackgroundBitmap
) 

Previous Tutorial:

How To ... Develop a Bitmap Painting Tool - Load and Save

NextTutorial:

How To ... Develop a Bitmap Painting Tool - Unwrap UV Coordinates

See Also