OSDN Git Service

add lock_alpha; blend mode refactoring
authorguciek <k@guciek.net>
Mon, 11 Apr 2011 13:34:42 +0000 (15:34 +0200)
committerMartin Renold <martinxyz@gmx.ch>
Mon, 16 May 2011 10:46:42 +0000 (12:46 +0200)
Squashed branch "blend_modes" by guciek into a single
commit, without modification. Based on 9818189 from
http://github.com/guciek/mypaint.git

Squashed log messages:
* add lock_alpha to brush setting window
* fix calls to removed function
* updated backend for blend modes
* switching blend modes
* a try at gui for blend modes
* pluggable blend modes in draw_dab
* quick hack to test feature

brushlib/brush.hpp
brushlib/brushsettings.py
brushlib/surface.hpp
gui/brushsettingswindow.py
gui/document.py
gui/drawwindow.py
gui/menu.xml
lib/tiledsurface.hpp

index 21de8d7..132f84a 100644 (file)
@@ -478,7 +478,8 @@ private:
     // the functions below will CLAMP most inputs
     hsv_to_rgb_float (&color_h, &color_s, &color_v);
     return surface->draw_dab (x, y, radius, color_h, color_s, color_v, opaque, hardness, eraser_target_alpha,
-                              states[STATE_ACTUAL_ELLIPTICAL_DAB_RATIO], states[STATE_ACTUAL_ELLIPTICAL_DAB_ANGLE]);
+                              states[STATE_ACTUAL_ELLIPTICAL_DAB_RATIO], states[STATE_ACTUAL_ELLIPTICAL_DAB_ANGLE],
+                              settings_value[BRUSH_LOCK_ALPHA]);
   }
 
   // How many dabs will be drawn between the current and the next (x, y, pressure, +dt) position?
index c4041be..aaf45e2 100644 (file)
@@ -79,6 +79,8 @@ settings_list = [
     ['elliptical_dab_ratio', _('Elliptical dab: ratio'), False, 1.0, 1.0, 10.0, _("aspect ratio of the dabs; must be >= 1.0, where 1.0 means a perfectly round dab. TODO: linearize? start at 0.0 maybe, or log?")],
     ['elliptical_dab_angle', _('Elliptical dab: angle'), False, 0.0, 90.0, 180.0, _("this defines the angle by which eliptical dabs are tilted\n 0.0 horizontal dabs\n 45.0 45 degrees, turned clockwise\n 180.0 horizontal again")],
     ['direction_filter', _('Direction filter'), False, 0.0, 2.0, 10.0, _("a low value will make the direction input adapt more quickly, a high value will make it smoother")],
+
+    ['lock_alpha', _('Lock alpha'), False, 0.0, 0.0, 1.0, _("do not modify the alpha channel of the layer (paint only where there is paint already)\n 0.0 normal painting\n 0.5 half of the paint gets applied normally\n 1.0 alpha channel fully locked")],
     ]
 
 settings_hidden = 'color_h color_s color_v'.split()
index b38ac43..53c52db 100644 (file)
@@ -21,7 +21,8 @@ public:
                          float color_r, float color_g, float color_b,
                          float opaque, float hardness = 0.5,
                          float alpha_eraser = 1.0,
-                                        float aspect_ratio = 1.0, float angle = 0.0
+                         float aspect_ratio = 1.0, float angle = 0.0,
+                         float lock_alpha = 0.0
                          ) = 0;
 
   virtual void get_color (float x, float y, 
index a7e0921..ac349a4 100644 (file)
@@ -100,7 +100,7 @@ class Window(windowing.SubWindow):
 
         groups = [
             {'id' : 'basic',    'title' : _('Basic'),   'settings' : [ 'radius_logarithmic', 'radius_by_random', 'hardness', 'eraser', 'offset_by_random', 'elliptical_dab_angle', 'elliptical_dab_ratio', 'direction_filter' ]},
-            {'id' : 'opacity',  'title' : _('Opacity'), 'settings' : [ 'opaque', 'opaque_multiply', 'opaque_linearize' ]},
+            {'id' : 'opacity',  'title' : _('Opacity'), 'settings' : [ 'opaque', 'opaque_multiply', 'opaque_linearize', 'lock_alpha' ]},
             {'id' : 'dabs',     'title' : _('Dabs'),    'settings' : [ 'dabs_per_basic_radius', 'dabs_per_actual_radius', 'dabs_per_second' ]},
             {'id' : 'smudge',   'title' : _('Smudge'),  'settings' : [ 'smudge', 'smudge_length', 'smudge_radius_log' ]},
             {'id' : 'speed',    'title' : _('Speed'),   'settings' : [ 'speed1_slowness', 'speed2_slowness', 'speed1_gamma', 'speed2_gamma', 'offset_by_speed', 'offset_by_speed_slowness' ]},
index 86ea45a..3b6b387 100644 (file)
@@ -60,10 +60,9 @@ class Document(object):
         self.tdw.device_observers.append(self.device_changed_cb)
         self.input_stroke_ended_observers.append(self.input_stroke_ended_cb)
         self.last_pen_device = None
-        self.last_used_devbrush_was_eraser = None
 
         # Eraser mode
-        self.eraser_mode_original_settings = None
+        self.eraser_mode_original_radius = None
 
         self.init_actions()
         self.init_context_actions()
@@ -83,7 +82,6 @@ class Document(object):
             ('Smaller',      None, _('Smaller'), 'd', None, self.brush_smaller_cb),
             ('MoreOpaque',   None, _('More Opaque'), 's', None, self.more_opaque_cb),
             ('LessOpaque',   None, _('Less Opaque'), 'a', None, self.less_opaque_cb),
-            ('Eraser',       None, _('Toggle Eraser Mode'), 'e', None, self.eraser_cb), # TODO: make toggle action
             ('PickContext',  None, _('Pick Context (layer, brush and color)'), 'w', None, self.pick_context_cb),
 
             ('Darker',       None, _('Darker'), None, None, self.darker_cb),
@@ -125,6 +123,11 @@ class Document(object):
             ('MirrorVertical', None, _('Mirror Vertical'), 'u', None, self.mirror_vertical_cb),
             ('SoloLayer',    None, _('Layer Solo'), 'Home', None, self.solo_layer_cb), # TODO: make toggle action
             ('ToggleAbove',  None, _('Hide Layers Above Current'), 'End', None, self.toggle_layers_above_cb), # TODO: make toggle action
+
+            ('BlendMode',    None, _('Blend Mode')),
+            ('BlendModeNormal', None, _('Normal'), None, None, self.blend_mode_cb),
+            ('BlendModeEraser', None, _('Eraser'), 'e', None, self.blend_mode_cb),
+            ('BlendModeLockAlpha', None, _('Lock alpha channel'), None, None, self.blend_mode_cb),
         ]
         ag = self.action_group = gtk.ActionGroup('DocumentActions')
         ag.add_actions(actions)
@@ -246,7 +249,7 @@ class Document(object):
         self.model.set_brush(self.app.brush)
 
     def brush_selected_cb(self, brush):
-        self.forget_eraser_mode()
+        self.auto_reset_blend_mode()
 
     def pick_context_cb(self, action):
         x, y = self.tdw.get_cursor_in_model_coordinates()
@@ -269,7 +272,6 @@ class Document(object):
                     picked_brush = ManagedBrush(self.app.brushmanager)
                     picked_brush.brushinfo.parse(si.brush_string)
                     self.app.brushmanager.select_brush(picked_brush)
-                    self.forget_eraser_mode()
                     self.si = si # FIXME: should be a method parameter?
                     self.strokeblink_state.activate(action)
                 return
@@ -361,14 +363,14 @@ class Document(object):
         adj.set_value(adj.get_value() / 1.8)
 
     def brighter_cb(self, action):
-        self.end_eraser_mode()
+        self.auto_reset_blend_mode()
         h, s, v = self.app.brush.get_color_hsv()
         v += 0.08
         if v > 1.0: v = 1.0
         self.app.brush.set_color_hsv((h, s, v))
 
     def darker_cb(self, action):
-        self.end_eraser_mode()
+        self.auto_reset_blend_mode()
         h, s, v = self.app.brush.get_color_hsv()
         v -= 0.08
         # stop a little higher than 0.0, to avoid resetting hue to 0
@@ -442,8 +444,6 @@ class Document(object):
             color = self.app.brush.get_color_hsv()
             bm.select_brush(context)
             self.app.brush.set_color_hsv(color)
-            # XXX
-            self.forget_eraser_mode()
 
     # TDW view manipulation
     def dragfunc_translate(self, dx, dy, x, y):
@@ -583,56 +583,10 @@ class Document(object):
     def no_double_buffering_cb(self, action):
         self.tdw.set_double_buffered(not action.get_active())
 
-
-    # ERASER
-    def eraser_cb(self, action):
-        if self.eraser_mode_original_settings:
-            self.end_eraser_mode()
-            return
-        adj = self.app.brush_adjustment['eraser']
-        e = adj.get_value()
-        if e > 0.9:
-            print "can't enter eraser mode if the current brush is an eraser"
-            self.tdw.window.beep()  # TODO: better feedback
-            return
-        # enter eraser mode
-        adj.set_value(1.0)
-        adj2 = self.app.brush_adjustment['radius_logarithmic']
-        r = adj2.get_value()
-        self.eraser_mode_original_settings = (e, r)
-        dr = self.app.preferences.get(ERASER_MODE_RADIUS_CHANGE_PREF, ERASER_MODE_RADIUS_CHANGE_DEFAULT)
-        adj2.set_value(r + dr)
-        # TODO: feedback to the user: "just shrink the brush three steps if this is too big"?
-
-    def end_eraser_mode(self):
-        if not self.eraser_mode_original_settings:
-            return
-        orig_e, orig_r = self.eraser_mode_original_settings
-        adj = self.app.brush_adjustment['eraser']
-        adj.set_value(orig_e)
-        # save eraser mode radius change, restore old radius
-        adj2 = self.app.brush_adjustment['radius_logarithmic']
-        r = adj2.get_value()
-        self.app.preferences[ERASER_MODE_RADIUS_CHANGE_PREF] = r - orig_r
-        adj2.set_value(orig_r)
-        self.forget_eraser_mode()
-
-    def forget_eraser_mode(self):
-        # Forget eraser mode states without altering the current brush.
-        # Do this always after restoring something from a saved brush.
-        self.eraser_mode_original_settings = None
-
+    # BLEND MODES
     def clone_selected_brush_for_saving(self):
-        # Clones the current brush as a new, nameless brush with all eraser
-        # mode settings de-applied.
-        settings_override = {}
-        clone = self.app.brushmanager.clone_selected_brush(name=None)
-        if self.eraser_mode_original_settings:
-            orig_e, orig_r = self.eraser_mode_original_settings
-            i = clone.brushinfo
-            i['eraser']             = (orig_e, i.get('eraser',             (None, {}))[1])
-            i['radius_logarithmic'] = (orig_r, i.get('radius_logarithmic', (None, {}))[1])
-        return clone
+        # Clones the current brush, along with blend mode settings.
+        return self.app.brushmanager.clone_selected_brush(name=None)
 
     def device_is_eraser(self, device):
         if device is None: return False
@@ -654,16 +608,12 @@ class Document(object):
 
         bm = self.app.brushmanager
         if old_device:
-            if self.device_is_eraser(old_device):
-                old_brush = bm.clone_selected_brush(name=None)  # preserve user-chosen eraser mode settings
-            else:
-                old_brush = self.clone_selected_brush_for_saving()  # discard eraser mode settings
+            old_brush = self.clone_selected_brush_for_saving()
             bm.store_brush_for_device(old_device.name, old_brush)
 
         if new_device.source == gdk.SOURCE_MOUSE:
             # Avoid fouling up unrelated devbrushes at stroke end
             self.app.preferences.pop('devbrush.last_used', None)
-            self.last_used_devbrush_was_eraser = None
         else:
             # Select the brush and update the UI.
             # Use a sane default if there's nothing associated
@@ -675,9 +625,7 @@ class Document(object):
                 else:
                     brush = bm.get_default_brush()
             self.app.preferences['devbrush.last_used'] = new_device.name
-            self.last_used_devbrush_was_eraser = self.device_is_eraser(new_device)
             bm.select_brush(brush)
-        self.forget_eraser_mode()
 
     def input_stroke_ended_cb(self, event):
         # Store device-specific brush settings at the end of the stroke, not
@@ -688,11 +636,7 @@ class Document(object):
         device_name = self.app.preferences.get('devbrush.last_used', None)
         if device_name is None:
             return
-        if self.last_used_devbrush_was_eraser:
-            bm = self.app.brushmanager
-            selected_brush = bm.clone_selected_brush(name=None)  # preserve user-chosen eraser mode settings
-        else:
-            selected_brush = self.clone_selected_brush_for_saving()  # discard eraser mode settings
+        selected_brush = self.clone_selected_brush_for_saving()
         self.app.brushmanager.store_brush_for_device(device_name, selected_brush)
         # However it may be better to reflect any brush settings change into
         # the last-used devbrush immediately. The UI idea here is that the
@@ -700,5 +644,35 @@ class Document(object):
         # real-world tool that you're dipping into a palette, or modifying
         # using the sliders.
 
+    def blend_mode_cb(self, action):
+        self.set_blend_mode(action.get_name()[9:])
+
+    def auto_reset_blend_mode(self):
+        self.set_blend_mode('Normal')
+
+    def set_blend_mode(self, blend_mode):
+        adjs = {
+            'Eraser': self.app.brush_adjustment['eraser'],
+            'LockAlpha': self.app.brush_adjustment['lock_alpha'],
+        }
+
+        on = blend_mode in adjs and adjs[blend_mode].get_value() < 0.9
+       for adj in adjs.values():
+           adj.set_value(0.0)
+        if on:
+            adjs[blend_mode].set_value(1.0)
+
+        r = self.app.brush_adjustment['radius_logarithmic']
+
+        if adjs['Eraser'].get_value() < 0.9 and self.eraser_mode_original_radius:
+            self.eraser_mode_radius_change = r.get_value() - self.eraser_mode_original_radius
+            r.set_value(self.eraser_mode_original_radius)
+            self.eraser_mode_original_radius = None
+
+        if adjs['Eraser'].get_value() >= 0.9 and not self.eraser_mode_original_radius:
+            self.eraser_mode_original_radius = r.get_value()
+            dr = self.app.preferences.get(ERASER_MODE_RADIUS_CHANGE_PREF, ERASER_MODE_RADIUS_CHANGE_DEFAULT)
+            r.set_value(r.get_value() + dr)
+
     def frame_changed_cb(self):
         self.tdw.queue_draw()
index 3c47876..8dfcf3f 100644 (file)
@@ -383,7 +383,7 @@ class Window (windowing.MainWindow, layout.MainWindow):
         # then enter them the usual way.
         if action_name in self.popup_states:
             state = self.popup_states[action_name]
-            self.app.doc.end_eraser_mode()
+            self.app.doc.auto_reset_blend_mode()
             state.activate(event)
             return True
 
@@ -443,7 +443,7 @@ class Window (windowing.MainWindow, layout.MainWindow):
         # This doesn't really belong here...
         # just because all popups are color popups now...
         # ...maybe should eraser_mode be a GUI state too?
-        self.app.doc.end_eraser_mode()
+        self.app.doc.auto_reset_blend_mode()
 
         state = self.popup_states[action.get_name()]
         state.activate(action)
index 6976406..fb1142b 100644 (file)
       <menuitem action='MoreOpaque'/>
       <menuitem action='LessOpaque'/>
       <separator/>
-      <menuitem action='Eraser'/>
+      <menu action='BlendMode'>
+        <menuitem action='BlendModeNormal'/>
+        <menuitem action='BlendModeEraser'/>
+        <menuitem action='BlendModeLockAlpha'/>
+      </menu>
       <separator/>
       <menuitem action='PickContext'/>
       <separator/>
index 18eb3b2..daa3695 100644 (file)
@@ -26,6 +26,60 @@ private:
   TileMemory tileMemory[TILE_MEMORY_SIZE];
   int tileMemoryValid;
   int tileMemoryWrite;
+  
+  // Containers for inline functions; with compiler optimization
+  // they will allow for diffrent versions of draw_dab()
+  // without code duplication or performance loss.
+
+  struct BlendMode_Normal {
+    inline static void pixel (uint16_t * rgba,
+                              const uint32_t & color_r_,
+                              const uint32_t & color_g_,
+                              const uint32_t & color_b_,
+                              const float & opa) {
+      uint32_t opa_a = (1<<15)*opa;   // topAlpha
+      uint32_t opa_b = (1<<15)-opa_a; // bottomAlpha
+
+      rgba[3] = opa_a + (opa_b*rgba[3])/(1<<15);
+      rgba[0] = (opa_a*color_r_ + opa_b*rgba[0])/(1<<15);
+      rgba[1] = (opa_a*color_g_ + opa_b*rgba[1])/(1<<15);
+      rgba[2] = (opa_a*color_b_ + opa_b*rgba[2])/(1<<15);
+    }
+  };
+
+  struct BlendMode_Eraser {
+    inline static void pixel (uint16_t * rgba,
+                              const uint32_t & color_r_,
+                              const uint32_t & color_g_,
+                              const uint32_t & color_b_,
+                              const float & opa) {
+      uint32_t opa_b = (1<<15)*opa;
+      opa_b = (1<<15)-opa_b;
+
+      rgba[3] = (opa_b*rgba[3])/(1<<15);
+      rgba[0] = (opa_b*rgba[0])/(1<<15);
+      rgba[1] = (opa_b*rgba[1])/(1<<15);
+      rgba[2] = (opa_b*rgba[2])/(1<<15);
+    }
+  };
+
+  struct BlendMode_LockAlpha {
+    inline static void pixel (uint16_t * rgba,
+                              const uint32_t & color_r_,
+                              const uint32_t & color_g_,
+                              const uint32_t & color_b_,
+                              const float & opa) {
+      uint32_t opa_a = (1<<15)*opa;   // topAlpha
+      uint32_t opa_b = (1<<15)-opa_a; // bottomAlpha
+
+      opa_a *= rgba[3];
+      opa_a /= (1<<15);
+
+      rgba[0] = (opa_a*color_r_ + opa_b*rgba[0])/(1<<15);
+      rgba[1] = (opa_a*color_g_ + opa_b*rgba[1])/(1<<15);
+      rgba[2] = (opa_a*color_b_ + opa_b*rgba[2])/(1<<15);
+    }
+  };
 
 public:
   TiledSurface(PyObject * self_) {
@@ -107,7 +161,52 @@ public:
                  float color_r, float color_g, float color_b,
                  float opaque, float hardness = 0.5,
                  float eraser_target_alpha = 1.0,
-                 float aspect_ratio = 1.0, float angle = 0.0) {
+                 float aspect_ratio = 1.0, float angle = 0.0,
+                 float lock_alpha = 0.0
+                 ) {
+
+    opaque = CLAMP(opaque, 0.0, 1.0);
+    hardness = CLAMP(hardness, 0.0, 1.0);
+    if (opaque == 0.0) return false;
+    if (radius < 0.1) return false;
+    if (hardness == 0.0) return false; // infintly small point, rest transparent
+    assert(atomic > 0);
+
+    float normal = 1.0;
+
+    float eraser = CLAMP(1.0 - eraser_target_alpha, 0.0, 1.0);
+    normal *= 1.0-eraser;
+
+    lock_alpha = CLAMP(lock_alpha, 0.0, 1.0);
+    normal *= 1.0-lock_alpha;
+    eraser *= 1.0-lock_alpha;
+
+    bool surface_modified = false;
+
+    if (normal > 0.00001)
+      surface_modified |= draw_dab_pixels<BlendMode_Normal>
+        (x, y, radius, color_r, color_g, color_b, opaque*normal, hardness,
+         aspect_ratio, angle);
+
+    if (eraser > 0.00001)
+      surface_modified |= draw_dab_pixels<BlendMode_Eraser>
+        (x, y, radius, color_r, color_g, color_b, opaque*eraser, hardness,
+         aspect_ratio, angle);
+
+    if (lock_alpha > 0.00001)
+      surface_modified |= draw_dab_pixels<BlendMode_LockAlpha>
+        (x, y, radius, color_r, color_g, color_b, opaque*lock_alpha, hardness,
+         aspect_ratio, angle);
+
+    return surface_modified;
+  }
+  
+  template<class BlendMode>
+  inline bool draw_dab_pixels (float x, float y, 
+                               float radius, 
+                               float color_r, float color_g, float color_b,
+                               float opaque, float hardness = 0.5,
+                               float aspect_ratio = 1.0, float angle = 0.0) {
 
        if (aspect_ratio<1.0) aspect_ratio=1.0;
 
@@ -116,7 +215,6 @@ public:
     float xx, yy, rr;
     float one_over_radius2;
 
-    eraser_target_alpha = CLAMP(eraser_target_alpha, 0.0, 1.0);
     uint32_t color_r_ = color_r * (1<<15);
     uint32_t color_g_ = color_g * (1<<15);
     uint32_t color_b_ = color_b * (1<<15);
@@ -124,14 +222,6 @@ public:
     color_g = CLAMP(color_g, 0, (1<<15));
     color_b = CLAMP(color_b, 0, (1<<15));
 
-    opaque = CLAMP(opaque, 0.0, 1.0);
-    hardness = CLAMP(hardness, 0.0, 1.0);
-    if (opaque == 0.0) return false;
-    if (radius < 0.1) return false;
-    if (hardness == 0.0) return false; // infintly small point, rest transparent
-
-    assert(atomic > 0);
-
     r_fringe = radius + 1;
     rr = radius*radius;
     one_over_radius2 = 1.0/rr;
@@ -194,26 +284,15 @@ public:
               // resultAlpha = topAlpha + (1.0 - topAlpha) * bottomAlpha
               // resultColor = topColor + (1.0 - topAlpha) * bottomColor
               //
-              // (at least for the normal case where eraser_target_alpha == 1.0)
-              // OPTIMIZE: separate function for the standard case without erasing?
               // OPTIMIZE: don't use floats here in the inner loop?
 
 #ifdef HEAVY_DEBUG
               assert(opa >= 0.0 && opa <= 1.0);
               assert(eraser_target_alpha >= 0.0 && eraser_target_alpha <= 1.0);
 #endif
-
-              uint32_t opa_a = (1<<15)*opa;   // topAlpha
-              uint32_t opa_b = (1<<15)-opa_a; // bottomAlpha
-              
-              // only for eraser, or for painting with translucent-making colors
-              opa_a *= eraser_target_alpha;
-              
               int idx = (yp*TILE_SIZE + xp)*4;
-              rgba_p[idx+3] = opa_a + (opa_b*rgba_p[idx+3])/(1<<15);
-              rgba_p[idx+0] = (opa_a*color_r_ + opa_b*rgba_p[idx+0])/(1<<15);
-              rgba_p[idx+1] = (opa_a*color_g_ + opa_b*rgba_p[idx+1])/(1<<15);
-              rgba_p[idx+2] = (opa_a*color_b_ + opa_b*rgba_p[idx+2])/(1<<15);
+              
+              BlendMode::pixel(rgba_p+idx, color_r_, color_g_, color_b_, opa);
             }
           }
         }