1 //
2 // EraserTool.cs
3 //
4 // Author:
5 //       Jonathan Pobst <monkey@jpobst.com>
6 //
7 // Copyright (c) 2010 Jonathan Pobst
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining a copy
10 // of this software and associated documentation files (the "Software"), to deal
11 // in the Software without restriction, including without limitation the rights
12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the Software is
14 // furnished to do so, subject to the following conditions:
15 //
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
18 //
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 // THE SOFTWARE.
26 
27 using System;
28 using Cairo;
29 using Gtk;
30 using Pinta.Core;
31 using Mono.Unix;
32 
33 namespace Pinta.Tools
34 {
35     public class EraserTool : BaseBrushTool
36     {
37         private enum EraserType
38         {
39             Normal = 0,
40             Smooth = 1,
41         }
42 
43         private Point last_point = point_empty;
44         private EraserType eraser_type = EraserType.Normal;
45 
46         private const int LUT_Resolution = 256;
47         private byte[][] lut_factor = null;
48 
49         private ToolBarLabel label_type = null;
50         private ToolBarComboBox comboBox_type = null;
51 
EraserTool()52         public EraserTool ()
53         {
54         }
55 
initLookupTable()56         private void initLookupTable()
57         {
58             if (lut_factor == null) {
59                 lut_factor = new byte[LUT_Resolution + 1][];
60 
61                 for (int dy = 0; dy < LUT_Resolution+1; dy++) {
62                     lut_factor [dy] = new byte[LUT_Resolution + 1];
63                     for (int dx = 0; dx < LUT_Resolution+1; dx++) {
64                         double d = Math.Sqrt (dx * dx + dy * dy) / LUT_Resolution;
65                         if (d > 1.0)
66                             lut_factor [dy][dx] = 255;
67                         else
68                             lut_factor [dy][dx] = (byte)(255.0 - Math.Cos (Math.Sqrt (d) * Math.PI / 2.0) * 255.0);
69                     }
70                 }
71             }
72         }
73 
copySurfacePart(ImageSurface surf, Gdk.Rectangle dest_rect)74         private ImageSurface copySurfacePart(ImageSurface surf, Gdk.Rectangle dest_rect)
75         {
76             ImageSurface tmp_surface = CairoExtensions.CreateImageSurface (Format.Argb32, dest_rect.Width, dest_rect.Height);
77 
78             using (Context g = new Context (tmp_surface)) {
79                 g.Operator = Operator.Source;
80                 g.SetSourceSurface (surf, -dest_rect.Left, -dest_rect.Top);
81                 g.Rectangle (new Rectangle (0, 0, dest_rect.Width, dest_rect.Height));
82                 g.Fill ();
83             }
84             //Flush to make sure all drawing operations are finished
85             tmp_surface.Flush ();
86             return tmp_surface;
87         }
88 
pasteSurfacePart(Context g,ImageSurface tmp_surface, Gdk.Rectangle dest_rect)89         private void pasteSurfacePart(Context g,ImageSurface tmp_surface, Gdk.Rectangle dest_rect)
90         {
91             g.Operator = Operator.Source;
92             g.SetSourceSurface (tmp_surface, dest_rect.Left, dest_rect.Top);
93             g.Rectangle (new Rectangle (dest_rect.Left, dest_rect.Top, dest_rect.Width, dest_rect.Height));
94             g.Fill ();
95         }
96 
eraseNormal(Context g, PointD start, PointD end)97         private void eraseNormal(Context g, PointD start, PointD end)
98         {
99             g.Antialias = UseAntialiasing ? Antialias.Subpixel : Antialias.None;
100 
101             // Adding 0.5 forces cairo into the correct square:
102             // See https://bugs.launchpad.net/bugs/672232
103             g.MoveTo (start.X + 0.5, start.Y + 0.5);
104             g.LineTo (end.X + 0.5, end.Y + 0.5);
105 
106             // Right-click is erase to background color, left-click is transparent
107             if (mouse_button == 3) {
108                 g.Operator = Operator.Source;
109                 g.SetSourceColor (PintaCore.Palette.SecondaryColor);
110             }
111             else
112                 g.Operator = Operator.Clear;
113 
114             g.LineWidth = BrushWidth;
115             g.LineJoin = LineJoin.Round;
116             g.LineCap = LineCap.Round;
117 
118             g.Stroke ();
119         }
120 
eraseSmooth(ImageSurface surf, Context g, PointD start, PointD end)121         protected unsafe void eraseSmooth(ImageSurface surf, Context g, PointD start, PointD end)
122         {
123             int rad = (int)(BrushWidth / 2.0) + 1;
124             //Premultiply with alpha value
125             byte bk_col_a = (byte)(PintaCore.Palette.SecondaryColor.A * 255.0);
126             byte bk_col_r = (byte)(PintaCore.Palette.SecondaryColor.R * bk_col_a);
127             byte bk_col_g = (byte)(PintaCore.Palette.SecondaryColor.G * bk_col_a);
128             byte bk_col_b = (byte)(PintaCore.Palette.SecondaryColor.B * bk_col_a);
129             int num_steps = (int)start.Distance(end) / rad + 1;
130             //Initialize lookup table when first used (to prevent slower startup of the application)
131             initLookupTable ();
132 
133             for (int step = 0; step < num_steps; step++) {
134                 PointD pt = Utility.Lerp(start, end, (float)step / num_steps);
135                 int x = (int)pt.X, y = (int)pt.Y;
136 
137                 Gdk.Rectangle surface_rect = new Gdk.Rectangle (0, 0, surf.Width, surf.Height);
138                 Gdk.Rectangle brush_rect = new Gdk.Rectangle (x - rad, y - rad, 2 * rad, 2 * rad);
139                 Gdk.Rectangle dest_rect = Gdk.Rectangle.Intersect (surface_rect, brush_rect);
140 
141                 if ((dest_rect.Width > 0) && (dest_rect.Height > 0)) {
142                     //Allow Clipping through a temporary surface
143                     using (ImageSurface tmp_surface = copySurfacePart (surf, dest_rect)) {
144 
145                         for (int iy = dest_rect.Top; iy < dest_rect.Bottom; iy++) {
146                             ColorBgra* srcRowPtr = tmp_surface.GetRowAddressUnchecked (iy - dest_rect.Top);
147                             int dy = ((iy - y) * LUT_Resolution) / rad;
148                             if (dy < 0)
149                                 dy = -dy;
150                             byte[] lut_factor_row = lut_factor [dy];
151 
152                             for (int ix = dest_rect.Left; ix < dest_rect.Right; ix++) {
153                                 ColorBgra col = *srcRowPtr;
154                                 int dx = ((ix - x) * LUT_Resolution) / rad;
155                                 if (dx < 0)
156                                     dx = -dx;
157 
158                                 int force = lut_factor_row [dx];
159                                 //Note: premultiplied alpha is used!
160                                 if (mouse_button == 3) {
161                                     col.A = (byte)((col.A * force + bk_col_a * (255 - force)) / 255);
162                                     col.R = (byte)((col.R * force + bk_col_r * (255 - force)) / 255);
163                                     col.G = (byte)((col.G * force + bk_col_g * (255 - force)) / 255);
164                                     col.B = (byte)((col.B * force + bk_col_b * (255 - force)) / 255);
165                                 } else {
166                                     col.A = (byte)(col.A * force / 255);
167                                     col.R = (byte)(col.R * force / 255);
168                                     col.G = (byte)(col.G * force / 255);
169                                     col.B = (byte)(col.B * force / 255);
170                                 }
171                                 *srcRowPtr = col;
172                                 srcRowPtr++;
173                             }
174                         }
175                         //Draw the final result on the surface
176                         pasteSurfacePart (g, tmp_surface, dest_rect);
177                     }
178                 }
179             }
180         }
181 
OnBuildToolBar(Toolbar tb)182         protected override void OnBuildToolBar(Toolbar tb)
183         {
184             base.OnBuildToolBar(tb);
185 
186             if (label_type == null)
187                 label_type = new ToolBarLabel (string.Format (" {0}: ", Catalog.GetString ("Type")));
188             if (comboBox_type == null) {
189                 comboBox_type = new ToolBarComboBox (100, 0, false, Catalog.GetString ("Normal"), Catalog.GetString ("Smooth"));
190 
191                 comboBox_type.ComboBox.Changed += (o, e) =>
192                 {
193                     eraser_type = (EraserType)comboBox_type.ComboBox.Active;
194                 };
195             }
196             tb.AppendItem (label_type);
197             tb.AppendItem (comboBox_type);
198             // Change the cursor when the BrushWidth is changed.
199             brush_width.ComboBox.Changed += (sender, e) => SetCursor (DefaultCursor);
200         }
201 
202         #region Properties
203         public override string Name { get { return Catalog.GetString ("Eraser"); } }
204         public override string Icon { get { return "Tools.Eraser.png"; } }
205         public override string StatusBarText { get { return Catalog.GetString ("Left click to erase to transparent, right click to erase to secondary color. "); } }
206 
207         public override Gdk.Cursor DefaultCursor {
208             get {
209                 int iconOffsetX, iconOffsetY;
210                 var icon = CreateIconWithShape ("Cursor.Eraser.png",
211                                                 CursorShape.Ellipse, BrushWidth, 8, 22,
212                                                 out iconOffsetX, out iconOffsetY);
213                 return new Gdk.Cursor (Gdk.Display.Default, icon, iconOffsetX, iconOffsetY);
214             }
215         }
216         public override bool CursorChangesOnZoom { get { return true; } }
217 
218         public override Gdk.Key ShortcutKey { get { return Gdk.Key.E; } }
219         public override int Priority { get { return 27; } }
220         #endregion
221 
222         #region Mouse Handlers
OnMouseMove(object o, Gtk.MotionNotifyEventArgs args, Cairo.PointD new_pointd)223         protected override void OnMouseMove (object o, Gtk.MotionNotifyEventArgs args, Cairo.PointD new_pointd)
224         {
225             Point new_point = new Point ((int)new_pointd.X, (int)new_pointd.Y);
226 
227             Document doc = PintaCore.Workspace.ActiveDocument;
228 
229             if (mouse_button <= 0) {
230                 last_point = point_empty;
231                 return;
232             }
233 
234             if (last_point.Equals (point_empty))
235                 last_point = new_point;
236 
237             if (doc.Workspace.PointInCanvas (new_pointd))
238                 surface_modified = true;
239 
240             var surf = doc.CurrentUserLayer.Surface;
241             using (Context g = new Context (surf)) {
242 
243                 g.AppendPath (doc.Selection.SelectionPath);
244                 g.FillRule = FillRule.EvenOdd;
245                 g.Clip ();
246                 PointD last_pointd = new PointD (last_point.X, last_point.Y);
247 
248                 if (eraser_type == EraserType.Normal) {
249                     eraseNormal (g, last_pointd, new_pointd);
250                 }
251                 else if (eraser_type == EraserType.Smooth) {
252                     eraseSmooth(surf, g, last_pointd, new_pointd);
253                 }
254             }
255 
256             Gdk.Rectangle r = GetRectangleFromPoints (last_point, new_point);
257 
258             if (doc.Workspace.IsPartiallyOffscreen (r)) {
259                 doc.Workspace.Invalidate ();
260             } else {
261                 doc.Workspace.Invalidate (doc.ClampToImageSize (r));
262             }
263 
264             last_point = new_point;
265         }
266         #endregion
267     }
268 }
269