-- PAINT -- -- -- © April 2002 - December 2003, OpenSpark Interactive Ltd -- -- ---------------------------------------------------------------------- -- This behavior allows the user to paint by dragging the mouse over -- the bitmap sprite to which it is attached. The sprite will be set -- to the full rect of the stage. -- -- When the mouse is released, two image objects are created. These -- are the same size as the smallest rect that fits around the changed -- area. The first is the original unchanged image, trimmed to the -- size of the modified area: the second is the same area cropped from -- the modified image. -- -- Both these images can be saved by an instance of the "Paint Line" -- undo step script. Allowing many undo steps can saturate memory. -- You can set the number of undo steps in the UndoAction() handler of -- the Undo Broker movie script. -- -- While the user is painting, the puppetTempo is set to 130 Hz, -- faster than the rate at which the mouseLoc is updated on any of the -- machines it has been tested on. The score tempo is restored when -- the user releases the mouse. -- -- This technique is compatible with the way the system cursor is -- handled in Mac OS X. Using a tight repeat loop under Mac OS X -- might provoke the OS to display the "rotating CD-Rom" cursor, to -- indicate that the application is busy. Using a fast frame rate -- should provide sufficient idle time so that the OS is not tempted -- to intervene. -- -- The line is first painted directly to the screen image, so no extra -- memory is used at this stage, and screen refreshes are very rapid. -- When the mouse is released, the screen image is copied into the -- bitmap member that is used as a canvas. ---------------------------------------------------------------------- -- PROPERTY DECLARATIONS -- property brushImage property brushColor property useBgImage property bgColor -- Internal properties property pSprite -- the sprite to which this behavior is attached property pMember -- the bitmap member of the sprite property pSpriteRect -- stage rect of the bitmap sprite property pOffset -- the top left corner of the sprite property pBrushRect -- rect of pBrush image property pBrushColor -- current color of brush when using eraser property pHalfWidth -- half the width of brushImage property pHalfHeight -- half the height of brushImage property pMask -- [#maskImage: ] used to ignore -- white pixels in brushImage with no alpha property pOriginal -- original image of canvas if useBgImage is TRUE property pTempo -- the puppetTempo when mouse is down, if not, 0 property pLoc -- latest value of the mouseLoc property pErase -- TRUE if the user is erasing the painting -- Screen coordinates used to calculate the rect of the dirtied area: property pLeft property pTop property pRight property pBottom -- EVENT HANDLERS -- on beginSprite(me) me.mInitialize() end beginSprite on mouseDown(me) me.mStartPaint() end mouseDown on exitFrame(me) if pTempo then me.mPaint() end if end exitFrame -- PUBLIC METHODS -- on Paint_SetBrushImage(me, anImageOrMember) -------------------------- -- INPUT: should be an image object or a member -- with an image property -- ACTION: Adopts the image as the new brush -- OUTPUT: TRUE if the new brush image is adopted, FALSE if not -------------------------------------------------------------------- if pTempo then -- Don't allow the brush to change in mid stroke return FALSE end if case ilk(anImageOrMember) of #image: brushImage = anImageOrMember.duplicate() #member: case anImageOrMember.type of #bitmap, #flash, #vectorShape: brushImage = anImageOrMember.image.duplicate() otherwise: return FALSE end case otherwise: return FALSE end case -- Determine the new dimensions of the brush pBrushRect = brushImage.rect pHalfWidth = pBrushRect.width / 2.0 pHalfHeight = pBrushRect.height / 2.0 if brushImage.depth = 32 then -- The alphaChannel will define the shape of the image pMask = [] -- Use the current brush color tAlpha = brushImage.extractAlpha() brushImage.fill(brushImage.rect, brushColor) brushImage.setAlpha(tAlpha) else -- Ignore any white pixels pMask = [#maskImage: brushImage.createMask()] -- A 32-bit image is required to show the full range of colors tTemp = image(pBrushRect.width, pBrushRect.height, 32) tTemp.copyPixels(brushImage, pBrushRect, pBrushRect) brushImage = tTemp -- Use the current brush color brushImage.fill(brushImage.rect, brushColor) end if if pErase then -- The user chose a brush, so stop using the eraser me.mRestoreBrush() pErase = FALSE end if return TRUE end Paint_SetBrushImage on Paint_SetBrushColor(me, aColor) ----------------------------------- -- INPUT: should be an rgb color object. -- ACTION: Adopts the color for the whole area of the current brush. -- If the brush is a 32-bit image, the shape of the brush as -- defined by its alpha channel will be preserved. -- OUTPUT: TRUE if the new color is adopted, FALSE if not -------------------------------------------------------------------- if ilk(aColor, #color) then brushColor = aColor if brushImage.depth = 32 then tAlpha = brushImage.extractAlpha() brushImage.fill(brushImage.rect, brushColor) brushImage.setAlpha(tAlpha) else brushImage.fill(brushImage.rect, brushColor) end if -- The user chose a color, so stop using the eraser pErase = FALSE return TRUE end if return FALSE end Paint_SetBrushColor on Paint_UseEraser(me, aUseEraserFlag) ------------------------------- -- INPUT: will be considered to be FALSE unless it -- is TRUE -- ACTION: Sets the internal property pErase. -- While pErase is TRUE, if useBgImage is FALSE, the brush -- color will be set to the bgColor. If useBgImage is TRUE, -- the original image will be used to replace the current -- image. The current brushImage will be used as the -- eraser shape. -------------------------------------------------------------------- if aUseEraserFlag = TRUE then pErase = TRUE else pErase = FALSE end if end Paint_UseEraser on Paint_EraseAll(me) ------------------------------------------------ -- ACTION: Replaces the current image with the original image or -- color -------------------------------------------------------------------- tUndoImage = pMember.image.duplicate() if useBgImage then pMember.image = pOriginal else pMember.image.fill(0, 0, pMember.width, pMember.height, bgColor) end if -- ============================================================== -- -- The following lines activate a multiple undo feature. The -- Undo Broker, Undo Step and Paint Line scripts must be present for -- this to work. If not, comment out the following lines. tData = [ \ #member: pMember, \ #before: tUndoImage, \ #after: pMember.image.duplicate(), \ #rect: pMember.rect, \ #action: "Erase All" \ ] UndoAction("Paint Line", tData) -- in the Undo Broker movie script -- ============================================================== -- end Paint_EraseAll -- PRIVATE METHODS -- on mInitialize(me) --------------------------------------------------- -- SENT BY beginSprite() -- ACTION: Sets the brush image and the canvas image as required by -- the behavior parameters, and ensures that painting is -- limited to the sprite rect. -------------------------------------------------------------------- pSprite = sprite(me.spriteNum) pMember = pSprite.member pSpriteRect = pSprite.rect pOffset = point(pSprite.left, pSprite.top) -- Determine the rect of the stage area tRect = (the activeWindow).rect tWidth = tRect.width tHeight = tRect.height -- Adopt the chosen brush image if brushImage = #default then brushImage = me.mGetDefaultBrush() else brushImage = member(brushImage) if brushImage.type = #bitmap then me.Paint_SetBrushImage(brushImage) --brushImage = brushImage.image.duplicate() else -- the chosen image is no longer available: use default brushImage = me.mGetDefaultBrush() end if end if -- Determine the dimensions of the brush pBrushRect = brushImage.rect pHalfWidth = pBrushRect.width / 2.0 pHalfHeight = pBrushRect.height / 2.0 -- Initialize the canvas image if useBgImage then pOriginal = pMember.image.duplicate() else -- Fill the canvas with the background color pMember.image.fill(0, 0, pMember.width, pMember.height, bgColor) end if end mInitialize on mGetDefaultBrush(me) ---------------------------------------------- -- CALLED by mInitialize() -- OUTPUT: Returns a default hard-edged brush image with a diameter -- of 10 pixels. A 32-bit image is used to ensure that all -- brushColor values will be correctly applied. -------------------------------------------------------------------- tSize = 10 tBrush = image(tSize, tSize, 32, 8) tOptions = [#color: brushColor, #shapeType: #oval] tBrush.fill(0, 0, tSize, tSize, tOptions) tAlpha = image(tSize, tSize, 8) tOptions[#color] = rgb(0, 0, 0) tAlpha.fill(0, 0, tSize, tSize, tOptions) tBrush.setAlpha(tAlpha) pMask = [] -- not required with a 32-bit image return tBrush end mGetDefaultBrush on mRestoreBrush(me) ------------------------------------------------- -- SENT BY Paint_SetBrushImage(), mPaint() -- ACTION: Restores original brush color after use of eraser -------------------------------------------------------------------- if not useBgImage then me.Paint_SetBrushColor(pBrushColor) end if end mRestoreBrush on mStartPaint(me) --------------------------------------------------- -- SENT BY mouseDown() -- ACTION: Starts painting into the stage.image, and prepares to -- copy the painted area to this bitmap member once the -- mouse is released. -------------------------------------------------------------------- -- Prepare to poll for mouseLocs faster than they are updated pTempo = the frameTempo puppetTempo(130) -- Remember the first point pLoc = the mouseLoc -- Paint in the background color when erasing to a plain canvas if pErase then if not useBgImage then pBrushColor = brushColor me.Paint_SetBrushColor(bgColor) -- sets brushColor pErase = TRUE -- set to FALSE by Paint_SetBrushColor() end if end if -- Draw the first point me.mPaintStroke(pLoc, pLoc) -- Prepare rect of dirtied area (to be inflated later) pLeft = pLoc.locH pTop = pLoc.locV pRight = pLoc.locH pBottom = pLoc.locV end mStartPaint on mPaint(me) -------------------------------------------------------- -- SENT BY exitFrame() -- ACTION: Paints into the stage.image while the mouse is down, and -- copies the painted area to this bitmap member when the -- mouse is released. -------------------------------------------------------------------- if the mouseDown then -- The user is still drawing with the mouse tLoc = the mouseLoc if pLoc <> tLoc then -- The mouse has moved: draw a line between this point and the -- last point where the mouse was seen me.mPaintStroke(pLoc, tLoc) pLoc = tLoc -- Update the dirtied area (not accounting for stroke width) tLocH = pLoc.locH tLocV = pLoc.locV if pLeft > tLocH then pLeft = tLocH else if pRight < tLocH then pRight = tLocH end if if pTop > tLocV then pTop = tLocV else if pBottom < tLocV then pBottom = tLocV end if end if else -- The user has finished drawing with the mouse puppetTempo(pTempo) -- reset tempo pTempo = 0 if pErase then me.mRestoreBrush() end if -- Copy stage image to bitmap member me.mUpdateBitmap() end if end mPaint on mPaintStroke(me, aLoc1, aLoc2) ------------------------------------ -- SENT BY mStartPaint() and regularly by mPaint() while the user is -- dragging the mouse -- INPUT: and are points on the stage -- ACTION: creates a trail in the stage.image between the two -- points -------------------------------------------------------------------- if pErase then if useBgImage then return me.mCopyImageFromOriginal(aLoc1, aLoc2) end if end if tDelta = aLoc1 - aLoc2 tSteps = max(abs(tDelta.locH), abs(tDelta.locV), 1) tDelta = tDelta / float(tSteps) tImage = (the activeWindow).image repeat while tSteps tPoint = aLoc2 + tDelta * tSteps tRect = rect(tPoint, tPoint).inflate(pHalfWidth, pHalfHeight) tRect = tRect.intersect(pSpriteRect) tImage.copyPixels(brushImage, tRect, pBrushRect, pMask) tSteps = tSteps - 1 end repeat end mPaintStroke on mCopyImageFromOriginal(me, aLoc1, aLoc2) -------------------------- -- SENT BY mPaintStroke() if pErase and useBgImage are both TRUE -- ACTION: copies the appropriate area of the original image into -- the stage.image -------------------------------------------------------------------- -- Prepare an alphaChannel mask if available... tMask = [#maskOffset: point(0, 0)] if brushImage.depth = 32 then tMask[#maskImage] = brushImage.extractAlpha() else -- ... or a 1-bit mask if not tMask[#maskImage] = brushImage.createMask() end if tDelta = aLoc1 - aLoc2 tSteps = max(abs(tDelta.locH), abs(tDelta.locV), 1) tDelta = tDelta / float(tSteps) tImage = (the activeWindow).image repeat while tSteps tPoint = aLoc2 + tDelta * tSteps tStageRect = rect(tPoint, tPoint).inflate(pHalfWidth,pHalfHeight) tStageRect = tStageRect.intersect(pSpriteRect) -- Move the source rect and mask offset around the source image tRect = tStageRect.offset(-pOffset.locH, -pOffset.locV) tMask.maskOffset = point(tRect.left, tRect.top) tImage.copyPixels(pOriginal, tStageRect, tRect, tMask) tSteps = tSteps - 1 end repeat end mCopyImageFromOriginal on mUpdateBitmap(me) ------------------------------------------------- -- SENT BY mPaint() when the user finishes drawing -- ACTION: copies the changed area of the stage image to the -- bitmap member and prepares for a possible undo. -------------------------------------------------------------------- -- Calculate the dirtied rect of the stage tRect = rect(pLeft, pTop, pRight, pBottom) tRect = tRect.inflate(pHalfWidth, pHalfHeight) -- adjust for stroke -- Crop the dirtied rect to the rect of the stage, in case the -- sprite overlaps the edge of the stage. tRect = tRect.intersect(pSpriteRect) -- Copy the appropriate rect from the stage image tNewImage = (the activeWindow).image.crop(tRect) -- Adjust the position of tRect to account for the sprite loc tRect = tRect - rect(pOffset, pOffset) -- Copy the appropriate rect from the unchanged bitmap tUndoImage = pMember.image.crop(tRect) -- crop creates duplicate -- Paste the changed area into the bitmap pMember.image.copyPixels(tNewImage, tRect, tNewImage.rect) -- ============================================================== -- -- The following lines activate a multiple undo feature. The -- Undo Broker, Undo Step and Paint Line scripts must be present for -- this to work. If not, comment out the following lines. tData = [ \ #member: pMember, \ #before: tUndoImage, \ #after: tNewImage, \ #rect: tRect \ ] if pErase then tData[#action] = "Erase paint" end if UndoAction("Paint Line", tData) -- in the Undo Broker movie script -- ============================================================== -- end mUpdateBitmap -- UTILITY HANDLER -- on mGetMemberList(me) ------------------------------------------------ -- CALLED by getPropertyDescriptionList() -- OUTPUT: A list of all the bitmap members less than 32 x 32 pixels -------------------------------------------------------------------- tMemberList = [] tMaxCastLib = the number of castLibs repeat with tCastLib = 1 to tMaxCastLib tMaxMember = the number of members of castLib tCastLib repeat with tNumber = 1 to tMaxMember tMember = member(tNumber, tCastLib) if tMember.type = #bitmap then if tMember.height < 33 then if tMember.width < 33 then if tMember.name = "" then tMemberList.append(tMember) else tMemberList.append(tMember.name) end if end if end if end if end repeat end repeat return tMemberList end mGetMemberList -- BEHAVIOR PARAMETERS AND DESCRIPTION -- on isOKToAttach(me, aSpriteType, aSpriteNumber) if aSpriteType = #graphic then if sprite(aSpriteNumber).member.type = #bitmap then return TRUE end if end if return FALSE end isOKToAttach on getPropertyDescriptionList(me) tPropertyList =[:] if the currentSpriteNum then tBrushList = me.mGetMemberList(#bitmap) else tBrushList = [] end if tBrushList.addAt(1, #default) tPropertyList[ \ #brushImage] = [ \ #comment: "Brush image", \ #format: #string, \ #range: tBrushList, \ #default: tBrushList[1] \ ] tPropertyList[ \ #brushColor] = [ \ #comment: "Brush color (if default brush is used)", \ #format: #color, \ #default: rgb(0, 0, 0) \ ] tPropertyList[ \ #useBgImage] = [ \ #comment: "Use current image of member for canvas?", \ #format: #boolean, \ #default: FALSE \ ] tPropertyList[ \ #bgColor] = [ \ #comment: "Canvas color (if member image not used)", \ #format: #color, \ #default: rgb(255, 255, 204) \ ] return tPropertyList end getPropertyDescriptionList on getBehaviorTooltip me return \ "Use with bitmap sprites to allow the"&RETURN&\ "user to paint by dragging the mouse."&RETURN&\ "If the Undo Broker and associated"&RETURN&\ "scripts are available, paint and"&RETURN&\ "erase actions can be undone." end getBehaviorTooltip on getBehaviorDescription me return \ "PAINT"&RETURN&RETURN&\ "Use with bitmap sprites to allow the user to paint by dragging the mouse."&RETURN&RETURN&\ "PARAMETERS"&RETURN&\ " * Brush image "&RETURN&\ " * Brush color if default brush is used "&RETURN&\ " * Use current member image for canvas? "&RETURN&\ " * Canvas color if member image not used "&\ RETURN&RETURN&\ "PUBLIC METHODS"&RETURN&\ "=> Paint_SetBrushImage()"&RETURN&\ "=> Paint_SetBrushColor()"&RETURN&\ "=> Paint_UseEraser()"&RETURN&\ "=> Paint_EraseAll()"&RETURN&RETURN&\ "ASSOCIATED SCRIPTS"&RETURN&\ "If the Undo Broker, Undo Step and Paint Line scripts are available, paint and erase actions can be undone." end getBehaviorDescription