Skip to content

Instantly share code, notes, and snippets.

@arcollector
Last active March 8, 2020 17:31
Show Gist options
  • Save arcollector/155a8c751f65c15872fb to your computer and use it in GitHub Desktop.
Save arcollector/155a8c751f65c15872fb to your computer and use it in GitHub Desktop.
Seed scanline filling algorithm in JS
var download = function( arrayBuffer ) {
var $a = document.createElement( 'a' );
$a.setAttribute( 'download', +new Date() + '.bmp' );
$a.setAttribute( 'href', URL.createObjectURL( new Blob( [ arrayBuffer ], { type: 'application/octet-binary' } ) ) );
$a.style.display = 'none';
document.body.appendChild( $a );
$a.click();
document.body.removeChild( $a );
}
// **************************
// **************************
var bmp = function( width, height, canvas ) {
var long2LittleEndian = function( val ) {
return new Uint8Array( [ val & 0x000000ff, (val & 0x0000ff00) >> 8, (val & 0x00ff0000) >> 16, (val & 0xff000000) >> 32 ] );
};
var extraBytes = width % 4,
canvaSize = height*(width*3 + extraBytes),
fileSize = 54 + canvaSize,
fz = long2LittleEndian( fileSize ),
w = long2LittleEndian( width ),
h = long2LittleEndian( height ),
data = new Uint8Array( fileSize );
data.set( [
66,77, // 'BM'
fz[0],fz[1],fz[2],fz[3], // file size in long in little endian
0,0, // always 0
0,0, // always 0
54,0,0,0, // canvas image starting address, 54 for 24 bits images
40,0,0,0, // header size, always 40
w[0],w[1],w[2],w[3], // width in long in little endian
h[0],h[1],h[2],h[3], // height
1,0, // image planes, always 1
24,0, // 24 bits color depth
0,0,0,0, // compression type, 0 for uncompressed
0,0,0,0, // ignore these fields
0,0,0,0,
0,0,0,0,
0,0,0,0,
0,0,0,0,
], 0 );
var x, y,
i, address,
canvasBMP = new Uint8Array( canvaSize );
for( i = 0, y = height - 1; y >= 0; y-- ) {
for( x = 0; x < width; x++ ) {
address = 4*(y*width + x);
canvasBMP[i++] = canvas[address + 2];
canvasBMP[i++] = canvas[address + 1];
canvasBMP[i++] = canvas[address];
}
for( x = 0; x < extraBytes; x++ ) {
canvasBMP[i++] = 0;
}
}
data.set( canvasBMP, 54 );
return data;
}
// **************************
// **************************
var RGB = function( r, g, b ) {
this.r = r;
this.g = g;
this.b = b;
};
RGB.prototype.equals = function( rgb ) {
return this.r === rgb.r && this.g === rgb.g && this.b === rgb.b;
};
// **************************
// **************************
var Canvas = function( width, height ) {
this.data = new Uint8Array( width * height * 4 );
this.width = width;
this.height = height;
return this;
};
Canvas.prototype.getPixel = function( x, y ) {
if( x < 0 || y < 0 || x >= this.width || y >= this.height ) {
return null;
}
var address = 4*(y*this.width + x);
return new RGB( this.data[address], this.data[address+1], this.data[address+2] );
};
Canvas.prototype.setPixel = function( x, y, rgb ) {
if( x < 0 || y < 0 || x >= this.width || y >= this.height ) {
return;
}
var address = 4*(y*this.width + x);
this.data[address] = rgb.r;
this.data[address+1] = rgb.g;
this.data[address+2] = rgb.b;
this.data[address+3] = 255;
};
// **************************
// **************************
var scanlineSeedFilling = function( seedX, seedY, boundaryColor, fillColor, canvas ) {
var seedScanline = function( xLeft, xRight, y ) {
var x = xLeft, xEnter,
px,
pFlag;
while( x <= xRight ) {
// seed the scan line above
pFlag = false;
for( px = canvas.getPixel( x, y ); px && !px.equals( boundaryColor ) && !px.equals( fillColor ) && x < xRight; x++, px = canvas.getPixel( x, y ) ) {
pFlag = true;
}
if( pFlag ) {
if( x === xRight && px && !px.equals( boundaryColor ) && !px.equals( fillColor ) ) {
stack.push( { x: x, y: y } );
} else {
stack.push( { x: x - 1, y: y } );
}
}
// continue checking in case the span is interrupted
xEnter = x;
for( px = canvas.getPixel( x, y ); px && (px.equals( boundaryColor ) && px.equals( fillColor )) && x < xRight; x++, px = canvas.getPixel( x, y ) );
// make sure that the px coordinate is incremented
if( x === xEnter ) {
x++;
}
}
};
var stack = [];
stack.push( { x: seedX, y: seedY } );
while( stack.length ) {
// get the seed px and set it to the new value
var popElem = stack[stack.length-1],
x = popElem.x,
y = popElem.y;
canvas.setPixel( x, y, fillColor );
stack.length--; // pop
var saveX,
xLeft, xRight,
px;
// save the x coordinate of the seed px
saveX = x;
// fill the span to the left of the seed px
for( x--, px = canvas.getPixel( x, y ); px && !px.equals( boundaryColor ); x--, px = canvas.getPixel( x, y ) ) {
canvas.setPixel( x, y, fillColor );
}
// save the extreme left px
xLeft = x + 1;
// fill the span to the right of the seed px
x = saveX;
for( x++, px = canvas.getPixel( x, y ); px && !px.equals( boundaryColor ); x++, px = canvas.getPixel( x, y ) ) {
canvas.setPixel( x, y, fillColor );
}
// save the extreme right px
xRight = x - 1;
// check that scan line above is neither a polygon boundary nor has
// been previously completely filled; if not, seed the scan line
// start at the left edge of the scan line subspan
seedScanline( xLeft, xRight, y + 1 );
// check that the scan line below is ot a polygon boundary
// nor has been previously completely filled
seedScanline( xLeft, xRight, y - 1 );
}
};
// **************************
// **************************
var canvas = new Canvas( 320, 200 );
// draw a square center at origin (160,100)
var boundaryColor = new RGB( 255, 255, 255 ),
x = 0, y = 0;
for( ; x < 50; x++ ) {
canvas.setPixel( x+135, y+75, boundaryColor );
canvas.setPixel( x+135, y+50+75, boundaryColor );
}
x = 0; y = 0;
for( ; y < 50; y++ ) {
canvas.setPixel( x+135, y+75, boundaryColor );
canvas.setPixel( x+50+135, y+75, boundaryColor );
}
var fillColor = new RGB( 255, 0 ,0 );
scanlineSeedFilling( 160,100, boundaryColor,fillColor, canvas ); // fill the square
download( bmp( canvas.width, canvas.height, canvas.data ) );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment