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-08 20:20:22 +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       image    = app.image,
 43       mouse    = app.mouse,
 44       layerOp  = null,
 45       bufferOp = null;
 46 
 47   var stackMax = 10000; // maximum depth of stack
 48   var lines = []; // stack of lines
 49   var pixelNew, pixelNewStr;
 50 
 51   // Opera provides a custom 2D context for Canvas, which includes better-suited 
 52   // methods: getPixel(), putPixel() and lockCanvasUpdates(). These help the 
 53   // tool work faster.
 54   if (pwlib.browser.opera) {
 55     layerOp  = app.layer.canvas.getContext('opera-2dgame');
 56     bufferOp = app.buffer.canvas.getContext('opera-2dgame');
 57   }
 58 
 59   /**
 60    * The <code>preActivate</code> event handler. This method checks if the 
 61    * browser implements the <code>getImageData()</code> and 
 62    * <code>putImageData()</code> context methods.  If not, the color bucket tool 
 63    * cannot be used.
 64    */
 65   this.preActivate = function () {
 66     // The latest versions of all browsers which implement Canvas, also 
 67     // implement the getImageData() method. This was only a problem with some 
 68     // old versions (eg. Opera 9.2).
 69     if (!layer.getImageData || !layer.putImageData) {
 70       alert(app.lang.errorCbucketUnsupported);
 71       return false;
 72     } else {
 73       return true;
 74     }
 75   };
 76 
 77   /**
 78    * The <code>activate</code> event handler. Canvas shadow rendering is 
 79    * disabled.
 80    */
 81   this.activate = function () {
 82     app.shadowDisallow();
 83   };
 84 
 85   /**
 86    * The <code>deactivate</code> event handler. Canvas shadow rendering is 
 87    * allowed once again.
 88    */
 89   this.deactivate = function () {
 90     app.shadowAllow();
 91   };
 92 
 93   /**
 94    * The <code>click</code> and <code>contextmenu</code> event handler. This 
 95    * method performs the flood fill operation.
 96    *
 97    * @param {Event} ev The DOM Event object.
 98    */
 99   this.click = this.contextmenu = function (ev) {
100     // Allow the user to right-click or hold down the Shift key to use the 
101     // border color for filling the image.
102     if (ev.type === 'contextmenu' || ev.button === 2 || ev.shiftKey) {
103       var fillStyle = buffer.fillStyle;
104       buffer.fillStyle = buffer.strokeStyle;
105       buffer.fillRect(0, 0, 1, 1);
106       buffer.fillStyle = fillStyle;
107     } else {
108       buffer.fillRect(0, 0, 1, 1);
109     }
110 
111     if (bufferOp && 'getPixel' in bufferOp) {
112       pixelNew = bufferOp.getPixel(0, 0);
113       pixelNewStr = pixelNew;
114     } else {
115       pixelNew = buffer.getImageData(0, 0, 1, 1);
116       pixelNewStr = pixelNew.data[0] + ';' +
117                     pixelNew.data[1] + ';' +
118                     pixelNew.data[2] + ';' +
119                     pixelNew.data[3];
120     }
121 
122     buffer.clearRect(0, 0, 1, 1);
123 
124     if (layerOp && 'lockCanvasUpdates' in layerOp) {
125       layerOp.lockCanvasUpdates(true);
126     }
127 
128     var res = fill(mouse.x, mouse.y);
129 
130     if (layerOp && 'lockCanvasUpdates' in layerOp && 'updateCanvas' in layerOp) 
131     {
132       layerOp.lockCanvasUpdates(false);
133       layerOp.updateCanvas();
134     }
135 
136     if (res) {
137       app.historyAdd();
138     }
139 
140     return true;
141   };
142 
143   /**
144    * Fill the image with the current fill color, starting from the <var>x</var> 
145    * and <var>y</var> coordinates.
146    *
147    * @private
148    *
149    * @param {Number} x The x coordinate for the starting point.
150    * @param {Number} y The y coordinate for the starting point.
151    *
152    * @returns {Boolean} True if the image was filled, or false otherwise.
153    */
154   function fill (x, y) {
155     var start, x1, x2, dy, pixelOld, tmp;
156 
157     // Read pixel value at seed point.
158     pixelOld = pixelRead(x, y);
159     if (pixelOld === pixelNewStr || x < 0 || x > image.width || y < 0 ||
160         y > image.height) {
161       return false;
162     }
163 
164     pushLine(y, x, x, 1);      // needed in some cases
165     pushLine(y + 1, x, x, -1); // seed segment (popped 1st)
166 
167     while (lines.length > 0) {
168       // pop segment off stack and fill a neighboring scan line
169       tmp = lines.pop();
170       dy = tmp[3];
171       y  = tmp[0] + dy;
172       x1 = tmp[1];
173       x2 = tmp[2];
174 
175       // segment of scan line y-dy for x1 <= x <= x2 was previously filled, now 
176       // explore adjacent pixels in scan line y
177       x = x1;
178       for (x = x1; x >= 0 && pixelRead(x, y) === pixelOld; x--) {
179         pixelWrite(x, y);
180       }
181 
182       if (x >= x1) {
183         for (x++; x <= x2 && pixelRead(x, y) !== pixelOld; x++);
184         start = x;
185         if (x > x2) {
186           continue;
187         }
188 
189       } else {
190         start = x + 1;
191         if (start < x1) {
192           pushLine(y, start, x1 - 1, -dy); // leak on left?
193         }
194 
195         x = x1 + 1;
196       }
197 
198       do {
199         for (; x < image.width && pixelRead(x,y) === pixelOld; x++) {
200           pixelWrite(x, y);
201         }
202 
203         pushLine(y, start, x - 1, dy);
204         if (x > (x2 + 1)) {
205           pushLine(y, x2 + 1, x - 1, -dy);  // leak on right?
206         }
207 
208         for (x++; x <= x2 && pixelRead(x, y) !== pixelOld; x++);
209         start = x;
210 
211       } while (x <= x2);
212     }
213 
214     return true;
215   };
216 
217   var pushLine = function (y, xl, xr, dy) {
218     if (lines.length < stackMax && (y+dy) >= 0 && (y+dy) <= image.height) {
219       lines.push([y, xl, xr, dy]);
220     }
221   };
222 
223   // Pixel read and write methods. In Opera we use their proprietary methods.
224   if (layerOp && 'getPixel' in layerOp && 'setPixel' in layerOp) {
225     var pixelRead = function (x, y) {
226         return layerOp.getPixel(x, y);
227       },
228       pixelWrite = function (x, y) {
229         layerOp.setPixel(x, y, pixelNew);
230       };
231 
232   } else {
233     var pixelRead = function (x, y) {
234         var p = layer.getImageData(x, y, 1, 1).data;
235         // uh oh, stringify...
236         return p[0] + ';' + p[1] + ';' + p[2] + ';' + p[3];
237       },
238       pixelWrite = function (x, y) {
239         layer.putImageData(pixelNew, x, y);
240       };
241   }
242 };
243 
244 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
245 
246