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