fabric.js: Flood Fill

This Fiddle implements a flood fill for fabric.js. The flood filled area is added as a fabric-js object to the canvas, which can then be moved around.

by Ben Gillbanks

HTML

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<p class="intro">
Click <i>Flood fill</i>, then click anywhere in the image to flood fill the area under the cursor.<br>
Click <i>Default</i> to get default fabric.js behavior: you can select the flood-filled area as an (image) object now.<br>
You can (optionally) change fill color and fill tolerance.
</p>
<table cellpadding="6">
	<tr>
		<td>Mode</td>
		<td>
			<input type="radio" name="mode" value="fabric" id="mode1" checked> <label for="mode1">Default</label>
			<input type="radio" name="mode" value="floodFill" id="mode2"> <label for="mode2">Flood fill</label>
		</td>
	</tr>
	<tr>
		<td>Color</td>
		<td>
			<div class="color selected" data-color="#f00"></div>
			<div class="color" data-color="#0f0"></div>
			<div class="color" data-color="#00f"></div>
			<div class="color" data-color="#000"></div>
			<div class="color" data-color="#fff"></div>
			<div class="color" data-color="#0ff"></div>
			<div class="color" data-color="#f0f"></div>
			<div class="color" data-color="#ff0"></div>
		</td>
	</tr>
	<tr>
		<td>Tolerance</td>
		<td><input type="range" id="tolerance" min="0" max="50" value="2" step="1"></td>
	</tr>
	<tr>
		<td></td>
		<td>
			<button type="button" id="removeObject">Remove object</button>
		</td>
	</tr>
</table>
<div id="canvasContainer">
	<canvas id="c" width="600" height="400"></canvas>
</div>

CSS

body, input, button {
	font: 12pt Arial, Helvetica;
}

#canvasContainer {
	background-image: url(data:image/gif;base64,R0lGODlhCgAKAIAAAOLi4v///yH5BAAHAP8ALAAAAAAKAAoAAAIRhB2ZhxoM3GMSykqd1VltzxQAOw==);
	border: 2px solid #ccc;
	display: inline-block;
}

.intro {
  background-color: #ddd;
  border: 1px solid: #333;
  padding: 5px;
}

input[type=range], input[type=text] {
	width: 200px;
}

input[type=radio] {
	transform: scale(1.25);
}

div.color {
	display: inline-block;
	width: 16px;
	height: 16px;
	padding: 1px;
	border: 1px solid #000;
	cursor: pointer;
}

div.color.selected {
	padding: 0;
	border: 2px solid #000;
}

JavaScript

/*
 * FloodFill for fabric.js
 * @author Arjan Haverkamp (av01d)
 * @date October 2018
 */
 
 var FloodFill = {

	// Compare subsection of array1's values to array2's values, with an optional tolerance
	withinTolerance: function(array1, offset, array2, tolerance)
	{
		var length = array2.length,
			start = offset + length;
		tolerance = tolerance || 0;

		// Iterate (in reverse) the items being compared in each array, checking their values are
		// within tolerance of each other
		while(start-- && length--) {
			if(Math.abs(array1[start] - array2[length]) > tolerance) {
				return false;
			}
		}

		return true;
	},

	// The actual flood fill implementation
	fill: function(imageData, getPointOffsetFn, point, color, target, tolerance, width, height)
	{
	    var directions = [[1, 0], [0, 1], [0, -1], [-1, 0]],
			coords = [],
			points = [point],
			seen = {},
			key,
			x,
			y,
			offset,
			i,
			x2,
			y2,
			minX = -1,
			maxX = -1,
			minY = -1,
			maxY = -1;

		// Keep going while we have points to walk
		while (!!(point = points.pop())) {
			x = point.x;
			y = point.y;
			offset = getPointOffsetFn(x, y);

			// Move to next point if this pixel isn't within tolerance of the color being filled
			if (!FloodFill.withinTolerance(imageData, offset, target, tolerance)) {
				continue;
			}

			if (x > maxX) { maxX = x; }
			if (y > maxY) { maxY = y; }
			if (x < minX || minX == -1) { minX = x; }
			if (y < minY || minY == -1) { minY = y; }

			// Update the pixel to the fill color and add neighbours onto stack to traverse
			// the fill area
			i = directions.length;
			while (i--) {
				// Use the same loop for setting RGBA as for checking the neighbouring pixels
				if (i < 4) {
					imageData[offset + i] = color[i];
					coords[offset+i] = color[i];
				}

				// Get the new coordinate by adjusting x and y based on current step
				x2 = x + directions[i][0];
				y2 = y + directions[i][1];
				key = x2 + ',' + y2;

				// If new coordinate is out of bounds, or...