Switch to vector based flood fill
This helps avoid many issues: - Allow use of a low tolerance filling so that lighter shades of colors aren't ignored. - Avoid leaving that 1px boundary around the filled area using an appropriate stroke width - Allows saving flood-fill ops to SVG files
This commit is contained in:
parent
d84906bf9c
commit
21d9ec3650
|
@ -2,6 +2,8 @@ package com.simplemobiletools.draw.pro.extensions
|
|||
|
||||
import android.graphics.Bitmap
|
||||
import com.simplemobiletools.draw.pro.helpers.QueueLinearFloodFiller
|
||||
import com.simplemobiletools.draw.pro.helpers.VectorFloodFiller
|
||||
import com.simplemobiletools.draw.pro.models.MyPath
|
||||
|
||||
fun Bitmap.floodFill(color: Int, x: Int, y: Int, tolerance: Int = 10): Bitmap {
|
||||
val floodFiller = QueueLinearFloodFiller(this).apply {
|
||||
|
@ -12,3 +14,13 @@ fun Bitmap.floodFill(color: Int, x: Int, y: Int, tolerance: Int = 10): Bitmap {
|
|||
floodFiller.floodFill(x, y)
|
||||
return floodFiller.image!!
|
||||
}
|
||||
|
||||
fun Bitmap.vectorFloodFill(color: Int, x: Int, y: Int, tolerance: Int): MyPath {
|
||||
val floodFiller = VectorFloodFiller(this).apply {
|
||||
fillColor = color
|
||||
this.tolerance = tolerance
|
||||
}
|
||||
|
||||
floodFiller.floodFill(x, y)
|
||||
return floodFiller.path
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
package com.simplemobiletools.draw.pro.helpers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import com.simplemobiletools.draw.pro.models.MyPath
|
||||
import java.util.*
|
||||
|
||||
// Original algorithm by J. Dunlap http:// www.codeproject.com/KB/GDI-plus/queuelinearflood-fill.aspx
|
||||
// Java port by Owen Kaluza
|
||||
// Android port by Darrin Smith (Standard Android)
|
||||
class VectorFloodFiller(image: Bitmap) {
|
||||
val path = MyPath()
|
||||
|
||||
private var width = 0
|
||||
private var height = 0
|
||||
private var pixels: IntArray? = null
|
||||
|
||||
private lateinit var pixelsChecked: BooleanArray
|
||||
private lateinit var ranges: Queue<FloodFillRange>
|
||||
|
||||
var fillColor = 0
|
||||
var tolerance = 0
|
||||
private var startColorRed = 0
|
||||
private var startColorGreen = 0
|
||||
private var startColorBlue = 0
|
||||
|
||||
init {
|
||||
width = image.width
|
||||
height = image.height
|
||||
pixels = IntArray(width * height)
|
||||
image.getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
}
|
||||
|
||||
private fun prepare() {
|
||||
// Called before starting flood-fill
|
||||
pixelsChecked = BooleanArray(pixels!!.size)
|
||||
ranges = LinkedList()
|
||||
}
|
||||
|
||||
// Fills the specified point on the bitmap with the currently selected fill color.
|
||||
// int x, int y: The starting coordinates for the fill
|
||||
fun floodFill(x: Int, y: Int) {
|
||||
// Setup
|
||||
prepare()
|
||||
|
||||
// Get starting color.
|
||||
val startPixel = pixels!!.getOrNull(width * y + x) ?: return
|
||||
if (startPixel == fillColor) {
|
||||
// No-op.
|
||||
return
|
||||
}
|
||||
startColorRed = Color.red(startPixel)
|
||||
startColorGreen = Color.green(startPixel)
|
||||
startColorBlue = Color.blue(startPixel)
|
||||
|
||||
// Do first call to flood-fill.
|
||||
linearFill(x, y)
|
||||
|
||||
// Call flood-fill routine while flood-fill ranges still exist on the queue
|
||||
var range: FloodFillRange
|
||||
while (ranges.size > 0) {
|
||||
// Get Next Range Off the Queue
|
||||
range = ranges.remove()
|
||||
|
||||
// Check Above and Below Each Pixel in the flood-fill Range
|
||||
var downPxIdx = width * (range.Y + 1) + range.startX
|
||||
var upPxIdx = width * (range.Y - 1) + range.startX
|
||||
val upY = range.Y - 1 // so we can pass the y coordinate by ref
|
||||
val downY = range.Y + 1
|
||||
for (i in range.startX..range.endX) {
|
||||
// Start Fill Upwards
|
||||
// if we're not above the top of the bitmap and the pixel above this one is within the color tolerance
|
||||
if (range.Y > 0 && !pixelsChecked[upPxIdx] && isPixelColorWithinTolerance(upPxIdx)) {
|
||||
linearFill(i, upY)
|
||||
}
|
||||
|
||||
// Start Fill Downwards
|
||||
// if we're not below the bottom of the bitmap and the pixel below this one is within the color tolerance
|
||||
if (range.Y < height - 1 && !pixelsChecked[downPxIdx] && isPixelColorWithinTolerance(downPxIdx)) {
|
||||
linearFill(i, downY)
|
||||
}
|
||||
downPxIdx++
|
||||
upPxIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the furthermost left and right boundaries of the fill area
|
||||
// on a given y coordinate, starting from a given x coordinate, filling as it goes.
|
||||
// Adds the resulting horizontal range to the queue of flood-fill ranges,
|
||||
// to be processed in the main loop.
|
||||
//
|
||||
// int x, int y: The starting coordinates
|
||||
private fun linearFill(x: Int, y: Int) {
|
||||
// Find Left Edge of Color Area
|
||||
var lFillLoc = x // the location to check/fill on the left
|
||||
var pxIdx = width * y + x
|
||||
path.moveTo(x.toFloat(), y.toFloat())
|
||||
|
||||
while (true) {
|
||||
// fill with the color
|
||||
val newX = (pxIdx % width).toFloat()
|
||||
val newY = (pxIdx - newX) / width
|
||||
path.lineTo(newX, newY)
|
||||
|
||||
// indicate that this pixel has already been checked and filled
|
||||
pixelsChecked[pxIdx] = true
|
||||
|
||||
// de-increment
|
||||
lFillLoc-- // de-increment counter
|
||||
pxIdx-- // de-increment pixel index
|
||||
|
||||
// exit loop if we're at edge of bitmap or color area
|
||||
if (lFillLoc < 0 || pixelsChecked[pxIdx] || !isPixelColorWithinTolerance(pxIdx)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
lFillLoc++
|
||||
|
||||
// Find Right Edge of Color Area
|
||||
var rFillLoc = x // the location to check/fill on the left
|
||||
pxIdx = width * y + x
|
||||
while (true) {
|
||||
// fill with the color
|
||||
val newX = (pxIdx % width).toFloat()
|
||||
val newY = (pxIdx - newX) / width
|
||||
path.lineTo(newX, newY)
|
||||
|
||||
// indicate that this pixel has already been checked and filled
|
||||
pixelsChecked[pxIdx] = true
|
||||
|
||||
// increment
|
||||
rFillLoc++ // increment counter
|
||||
pxIdx++ // increment pixel index
|
||||
|
||||
// exit loop if we're at edge of bitmap or color area
|
||||
if (rFillLoc >= width || pixelsChecked[pxIdx] || !isPixelColorWithinTolerance(pxIdx)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
rFillLoc--
|
||||
|
||||
// add range to queue
|
||||
val r = FloodFillRange(lFillLoc, rFillLoc, y)
|
||||
ranges.offer(r)
|
||||
}
|
||||
|
||||
// Sees if a pixel is within the color tolerance range.
|
||||
private fun isPixelColorWithinTolerance(px: Int): Boolean {
|
||||
val red = pixels!![px] ushr 16 and 0xff
|
||||
val green = pixels!![px] ushr 8 and 0xff
|
||||
val blue = pixels!![px] and 0xff
|
||||
return red >= startColorRed - tolerance && red <= startColorRed + tolerance && green >= startColorGreen - tolerance && green <= startColorGreen + tolerance && blue >= startColorBlue - tolerance && blue <= startColorBlue + tolerance
|
||||
}
|
||||
|
||||
// Represents a linear range to be filled and branched from.
|
||||
private inner class FloodFillRange(var startX: Int, var endX: Int, var Y: Int)
|
||||
}
|
|
@ -17,7 +17,7 @@ import com.simplemobiletools.commons.extensions.toast
|
|||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
import com.simplemobiletools.draw.pro.R
|
||||
import com.simplemobiletools.draw.pro.extensions.contains
|
||||
import com.simplemobiletools.draw.pro.extensions.floodFill
|
||||
import com.simplemobiletools.draw.pro.extensions.vectorFloodFill
|
||||
import com.simplemobiletools.draw.pro.interfaces.CanvasListener
|
||||
import com.simplemobiletools.draw.pro.models.CanvasOp
|
||||
import com.simplemobiletools.draw.pro.models.MyParcelable
|
||||
|
@ -32,7 +32,7 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
private val MIN_ERASER_WIDTH = 20f
|
||||
private val MAX_HISTORY_COUNT = 1000
|
||||
private val BITMAP_MAX_HISTORY_COUNT = 60
|
||||
private val DEFAULT_FLOOD_FILL_TOLERANCE = 190
|
||||
private val FLOOD_FILL_TOLERANCE = 2
|
||||
|
||||
private val mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
|
||||
|
@ -397,8 +397,9 @@ class MyCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
|||
val color = mPaintOptions.color
|
||||
|
||||
ensureBackgroundThread {
|
||||
val img = bitmap.floodFill(color = color, x = touchedX, y = touchedY, tolerance = DEFAULT_FLOOD_FILL_TOLERANCE)
|
||||
addOperation(CanvasOp.BitmapOp(img))
|
||||
val path = bitmap.vectorFloodFill(color = color, x = touchedX, y = touchedY, tolerance = FLOOD_FILL_TOLERANCE)
|
||||
val paintOpts = PaintOptions(color = color, strokeWidth = 4f)
|
||||
addOperation(CanvasOp.PathOp(path, paintOpts))
|
||||
post { invalidate() }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue