Hey DaProd,
I made about 10 coloring book apps that are on the itunes store and sorry if some of this code contains irrelevant timers and bools but the necessary parts should be here. I figured I would share how I did the flood fill as I got a lot of help with filling a texture using CCMutableTexture2D authored by Lam Hoang Pham also posted in the forums here....
First off I created a CCMutableTexture2D from a UIImage when my drawing layer is shown:
ColoringImage = [[UIImage alloc] initWithContentsOfFile:file];
[CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA8888];
ColoringTexture = [[CCMutableTexture2D alloc] initWithImage:ColoringImage];
And below is the actual flood fill, which is done in a few steps, first it prepares the image by setting a 2d
array of bools to not checked. Then on the touchesEnded, adds the color and pixel to a Queue which will be
calling linear fill on the update. The linear fill scans horizontally and all the neighbors for a positive color change and sets the pixels with the newColor adding all neighbors to the Queue as well and marks it dirty.
You could do the whole image at once, but I found it better to watch the fill animate and is more responsive.
-(void)tick:(ccTime)dt
{
NoMoveTimer -= (float)dt;
NoFillTimer -= (float)dt;
if( lastUpdate <= 0 )
{
int count = 0;
while( [PixelQueue count] > 0 && count < 30)
{
ccRange range;
[[PixelQueue objectAtIndex:0] getValue:&range];
[PixelQueue removeObjectAtIndex:0];
int upY = range.Y - 1;
int downY = range.Y +1;
for( int i = range.Start; i<= range.End; i++ )
{
if( range.Y > 0 && !PixelsChecked[i][upY])
{
ccColor4B pixel = [ColoringTexture pixelAt:ccp(i,upY)];
if( [self CheckPixel:pixel] )
{
[self LinearFill: i : upY];
}
}
if( range.Y < [ColoringSprite contentSize].height-1 && !PixelsChecked[i][downY])
{
ccColor4B pixel = [ColoringTexture pixelAt:ccp(i,downY)];
if( [self CheckPixel:pixel] )
{
[self LinearFill: i : downY];
}
}
}
count++;
}
if( count > 0 )
{
[ColoringTexture apply];
ColoringSprite.dirty = YES;
}
}
}
-(void) LinearFill: (int) startX : (int) startY
{
//**********************************************************************
// Scan left
//**********************************************************************
int lFillLoc = startX;
while (true)
{
[ColoringTexture setPixelAt:ccp(lFillLoc,startY) rgba:TargetColor];
PixelsChecked[lFillLoc][startY] = YES;
lFillLoc--;
if( lFillLoc < 0 )
{
break;
}
if( PixelsChecked[lFillLoc][startY] )
{
break;
}
ccColor4B pixel = [ColoringTexture pixelAt:ccp(lFillLoc,startY)];
if(![self CheckPixel:pixel])
{
break;
}
}
lFillLoc++;
//**********************************************************************
// Scan right
//**********************************************************************
int rFillLoc = startX;
while (true)
{
[ColoringTexture setPixelAt:ccp(rFillLoc,startY) rgba:TargetColor];
PixelsChecked[rFillLoc][startY] = YES;
rFillLoc++;
if( rFillLoc >= [ColoringSprite contentSize].width )
{
break;
}
if( PixelsChecked[rFillLoc][startY] )
{
break;
}
ccColor4B pixel = [ColoringTexture pixelAt:ccp(rFillLoc,startY)];
if(![self CheckPixel:pixel] )
{
break;
}
}
rFillLoc--;
//add range to queue
ccRange range = ccr( lFillLoc, rFillLoc, startY);
NSValue* val = [NSValue value:&range withObjCType:@encode(ccRange)];
[PixelQueue addObject:val];
}
-(Boolean) CheckPixel: (ccColor4B)pixelA
{
if( pixelA.r == StartingColor.r &&
pixelA.g == StartingColor.g &&
pixelA.b == StartingColor.b )
{
return YES;
}
return NO;
}
-(void)FloodFillTexture :(CGPoint) UvTxtCoord
{
int x = UvTxtCoord.x;
int y = UvTxtCoord.y;
StartingColor = [ColoringTexture pixelAt:ccp(x,y)];
if (StartingColor.r == 0 && StartingColor.g == 0 && StartingColor.b == 0 )
{
return;
}
[self PrepareFill];
[self LinearFill: x : y];
}
//******************************************************
// Prepare linear queue algorithm
//******************************************************
-(void)PrepareFill
{
[[SimpleAudioEngine sharedEngine] playEffect:@"ColorPlopped.wav"];
while( [PixelQueue count] > 0 )
{
[PixelQueue removeObjectAtIndex:0];
}
int mw = [ColoringSprite contentSize].width;
int mh = [ColoringSprite contentSize].height;
for( int i = 0; i<mw; i++ )
{
for( int j = 0; j<mh; j++ )
{
PixelsChecked[i][j] = NO;
}
}
}
To actually being the flood fill, on touches ended I did this:
int pixelX = 0;
int pixelY = 0;
int width = [ColoringSprite contentSize].width * ColoringSprite.scaleX;
int height = [ColoringSprite contentSize].height * ColoringSprite.scaleY;
int rightX = ColoringSprite.position.x + width/2;
int leftX = ColoringSprite.position.x - width/2;
int topY = ColoringSprite.position.y + height/2;
int bottomY = ColoringSprite.position.y - height/2;
if ( (WolrdLoc.x < leftX || WolrdLoc.x > rightX) || (WolrdLoc.y < bottomY || WolrdLoc.y > topY ) )
{
}
else
{
if( WolrdLoc.x > leftX && WolrdLoc.x <= rightX)
{
int nx = WolrdLoc.x - leftX;
pixelX = (int)((float)nx/(float)ColoringSprite.scaleX);
}
if( WolrdLoc.y > bottomY && WolrdLoc.y <= topY)
{
int ny = topY - WolrdLoc.y;
pixelY = (int)((float)ny/(float)ColoringSprite.scaleY);
}
ccColor4B col = ccc4( Colors[SelectedCrayon].r,
Colors[SelectedCrayon].g,
Colors[SelectedCrayon].b,
255 );
if( [PixelQueue count] == 0 )
{
TargetColor = col;
if( TargetColor.r <= 15 && TargetColor.g <= 15 && TargetColor.b <= 15 )
{
NSLog(@"COLOR FILTER HIT");
TargetColor = ccc4(20,15,15,255);
}
if( EraserPushed )
{
TargetColor = ccc4(255, 255, 255, 255);
}
[self FloodFillTexture: ccp(pixelX, pixelY)];
[ColoringTexture apply];
ColoringSprite.dirty = YES;
}
}
And some of the helper structs like range are below:
typedef struct _ccRange
{
int Start;
int End;
int Y;
} ccRange;
static inline ccRange
ccr(const int start, const int end, const int y)
{
ccRange c = {start, end, y};
return c;
}
//******************************************************
// Layer for drawing screen
//******************************************************
@interface DrawingLayer : CCLayer {}
Boolean PixelsChecked[MaxImageDimension][MaxImageDimension];
CCMutableTexture2D* ColoringTexture;
NSMutableArray* PixelQueue;
Hopefully that helps,
~J