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