1 /*
  2  * Copyright (C) 2009 Mihai Şucan
  3  *
  4  * This file is part of PaintWeb.
  5  *
  6  * PaintWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * PaintWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
 18  *
 19  * $URL: http://code.google.com/p/paintweb $
 20  * $Date: 2009-11-10 20:12:34 +0200 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview Holds the color bucket tool implementation, also known as the 
 26  * flood fill tool.
 27  */
 28 
 29 /**
 30  * @class The color bucket tool.
 31  *
 32  * The implementation here is based on the seed fill algorithm of Paul S.  
 33  * Heckbert (1990).
 34  *
 35  * @param {PaintWeb} app Reference to the main paint application object.
 36  */
 37 pwlib.tools.cbucket = function (app) {
 38   var _self   = this,
 39       config  = app.config,
 40       layer   = app.layer.context,
 41       buffer  = app.buffer.context,
 42       iwidth  = app.image.width,
 43       iheight = app.image.height,
 44       mouse   = app.mouse;
 45 
 46   var stackMax = 10000; // maximum depth of stack
 47   var lines = []; // stack of lines
 48   var pixelNew, layerpix;
 49 
 50   /**
 51    * The <code>preActivate</code> event handler. This method checks if the 
 52    * browser implements the <code>getImageData()</code> and 
 53    * <code>putImageData()</code> context methods.  If not, the color bucket tool 
 54    * cannot be used.
 55    *
 56    * @returns {Boolean} True if the drawing tool can be activated, or false 
 57    * otherwise.
 58    */
 59   this.preActivate = function () {
 60     // The latest versions of all browsers which implement Canvas, also 
 61     // implement the getImageData() method. This was only a problem with some 
 62     // old versions (eg. Opera 9.2).
 63     if (!layer.getImageData || !layer.putImageData) {
 64       alert(app.lang.errorCbucketUnsupported);
 65       return false;
 66     } else {
 67       return true;
 68     }
 69   };
 70 
 71   /**
 72    * The <code>activate</code> event handler. Canvas shadow rendering is 
 73    * disabled.
 74    */
 75   this.activate = function () {
 76     app.shadowDisallow();
 77   };
 78 
 79   /**
 80    * The <code>deactivate</code> event handler. Canvas shadow rendering is 
 81    * allowed once again.
 82    */
 83   this.deactivate = function () {
 84     app.shadowAllow();
 85   };
 86 
 87   /**
 88    * The <code>click</code> and <code>contextmenu</code> event handler. This 
 89    * method performs the flood fill operation.
 90    *
 91    * @param {Event} ev The DOM Event object.
 92    *
 93    * @returns {Boolean} True if the image was modified, or false otherwise.
 94    */
 95   this.click = function (ev) {
 96     // Allow the user to right-click or hold down the Shift key to use the 
 97     // border color for filling the image.
 98     if (ev.type === 'contextmenu' || ev.button === 2 || ev.shiftKey) {
 99       var fillStyle = buffer.fillStyle;
100       buffer.fillStyle = buffer.strokeStyle;
101       buffer.fillRect(0, 0, 1, 1);
102       buffer.fillStyle = fillStyle;
103     } else {
104       buffer.fillRect(0, 0, 1, 1);
105     }
106 
107     // Instead of parsing the fillStyle ...
108     pixelNew = buffer.getImageData(0, 0, 1, 1);
109     pixelNew = [pixelNew.data[0], pixelNew.data[1], pixelNew.data[2], 
110              pixelNew.data[3]];
111 
112     buffer.clearRect(0, 0, 1, 1);
113 
114     var pixelOld = layer.getImageData(mouse.x, mouse.y, 1, 1).data;
115     pixelOld = pixelOld[0] + ';' + pixelOld[1] + ';' + pixelOld[2] + ';' 
116       + pixelOld[3];
117 
118     if (pixelOld === pixelNew.join(';')) {
119       return false;
120     }
121 
122     fill(mouse.x, mouse.y, pixelOld);
123 
124     app.historyAdd();
125 
126     return true;
127   };
128   this.contextmenu = this.click;
129 
130   /**
131    * Fill the image with the current fill color, starting from the <var>x</var> 
132    * and <var>y</var> coordinates.
133    *
134    * @private
135    *
136    * @param {Number} x The x coordinate for the starting point.
137    * @param {Number} y The y coordinate for the starting point.
138    * @param {String} pixelOld The old pixel value.
139    */
140   var fill = function (x, y, pixelOld) {
141     var start, x1, x2, dy, tmp, idata;
142 
143     pushLine(y, x, x, 1);      // needed in some cases
144     pushLine(y + 1, x, x, -1); // seed segment (popped 1st)
145 
146     while (lines.length > 0) {
147       // pop segment off stack and fill a neighboring scan line
148       tmp = lines.pop();
149       dy = tmp[3];
150       y  = tmp[0] + dy;
151       x1 = tmp[1];
152       x2 = tmp[2];
153 
154       layerpix = null;
155       idata = layer.getImageData(0, y, iwidth, 1);
156       layerpix = idata.data;
157 
158       // segment of scan line y-dy for x1 <= x <= x2 was previously filled, now 
159       // explore adjacent pixels in scan line y
160       for (x = x1; x >= 0 && pixelRead(x) === pixelOld; x--) {
161         pixelWrite(x);
162       }
163 
164       if (x >= x1) {
165         for (x++; x <= x2 && pixelRead(x) !== pixelOld; x++);
166         start = x;
167         if (x > x2) {
168           layer.putImageData(idata, 0, y);
169           continue;
170         }
171 
172       } else {
173         start = x + 1;
174         if (start < x1) {
175           pushLine(y, start, x1 - 1, -dy); // leak on left?
176         }
177 
178         x = x1 + 1;
179       }
180 
181       do {
182         for (; x < iwidth && pixelRead(x) === pixelOld; x++) {
183           pixelWrite(x);
184         }
185 
186         pushLine(y, start, x - 1, dy);
187         if (x > (x2 + 1)) {
188           pushLine(y, x2 + 1, x - 1, -dy);  // leak on right?
189         }
190 
191         for (x++; x <= x2 && pixelRead(x) !== pixelOld; x++);
192         start = x;
193 
194       } while (x <= x2);
195 
196       layer.putImageData(idata, 0, y);
197     }
198 
199     layerpix = null;
200     idata = null;
201   };
202 
203   var pushLine = function (y, xl, xr, dy) {
204     if (lines.length < stackMax && (y+dy) >= 0 && (y+dy) < iheight) {
205       lines.push([y, xl, xr, dy]);
206     }
207   };
208 
209   var pixelRead = function (x) {
210     var r = 4 * x;
211     return layerpix[r] + ';' + layerpix[r+1] + ';' + layerpix[r+2] + ';' 
212       + layerpix[r+3];
213   };
214 
215   var pixelWrite = function (x) {
216     var r = 4 * x;
217     layerpix[r]   = pixelNew[0];
218     layerpix[r+1] = pixelNew[1];
219     layerpix[r+2] = pixelNew[2];
220     layerpix[r+3] = pixelNew[3];
221   };
222 };
223 
224 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
225 
226