// 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?
['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()
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,
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' ]},
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()
('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),
('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)
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()
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
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
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):
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
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
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
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
# 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()
# 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
# 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)
<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/>
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_) {
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;
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);
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;
// 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);
}
}
}