8000 Supporting different alphas for face and edge colours by Westacular · Pull Request #1954 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Supporting different alphas for face and edge colours #1954

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 11, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/api/api_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ Changes in 1.3.x
* The :func:`~matplotlib.cbook.check_output` function has been moved to
`~matplotlib.compat.subprocess`.

* :class:`~matplotlib.patches.Patch` now fully supports using RGBA values for
its ``facecolor`` and ``edgecolor`` attributes, which enables faces and
edges to have different alpha values. If the
:class:`~matplotlib.patches.Patch` object's ``alpha`` attribute is set to
anything other than ``None``, that value will override any alpha-channel
value in both the face and edge colors. Previously, if
:class:`~matplotlib.patches.Patch` had ``alpha=None``, the alpha component
of ``edgecolor`` would be applied to both the edge and face.

* The optional ``isRGB`` argument to
:meth:`~matplotlib.backend_bases.GraphicsContextBase.set_foreground` (and
the other GraphicsContext classes that descend from it) has been renamed to
``isRGBA``, and should now only be set to ``True`` if the ``fg`` color
argument is known to be an RGBA tuple.

* For :class:`~matplotlib.patches.Patch`, the ``capstyle`` used is now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is SVG only, right?

``butt``, to be consistent with the default for most other objects, and to
avoid problems with non-solid ``linestyle`` appearing solid when using a
large ``linewidth``. Previously, :class:`~matplotlib.patches.Patch` used
``capstyle='projecting'``.

Changes in 1.2.x
================

Expand Down
10 changes: 10 additions & 0 deletions doc/users/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ Wes Campaigne and Phil Elson fixed the Agg backend such that PNGs are now
saved with the correct background color when :meth:`fig.patch.get_alpha` is
not 1.

Independent alpha values for face and edge colors
-------------------------------------------------
Wes Campaigne modified how :class:`~matplotlib.patches.Patch` objects are
drawn such that (for backends supporting transparency) you can set different
alpha values for faces and edges, by specifying their colors in RGBA format.
Note that if you set the alpha attribute for the patch object (e.g. using
:meth:`~matplotlib.patches.Patch.set_alpha` or the ``alpha`` keyword
argument), that value will override the alpha components set in both the
face and edge colors.
Copy link
Member

Nicely put. And sounds like sensible behaviour too. - that is a good sign 😉



.. _whats-new-1-2-2:

Expand Down
52 changes: 29 additions & 23 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,6 @@ def _iter_collection(self, gc, master_transform, all_transforms,
gc0 = self.new_gc()
gc0.copy_properties(gc)

original_alpha = gc.get_alpha()

if Nfacecolors == 0:
rgbFace = None

Expand All @@ -387,7 +385,6 @@ def _iter_collection(self, gc, master_transform, all_transforms,
yo = -(yp - yo)
if not (np.isfinite(xo) and np.isfinite(yo)):
continue
gc0.set_alpha(original_alpha)
if Nfacecolors:
rgbFace = facecolors[i % Nfacecolors]
if Nedgecolors:
Expand All @@ -400,16 +397,12 @@ def _iter_collection(self, gc, master_transform, all_transforms,
if fg[3] == 0.0:
gc0.set_linewidth(0)
else:
gc0.set_alpha(gc0.get_alpha() * fg[3])
gc0.set_foreground(fg[:3])
gc0.set_foreground(fg)
else:
gc0.set_foreground(fg)
if rgbFace is not None and len(rgbFace) == 4:
if rgbFace[3] == 0:
rgbFace = None
else:
gc0.set_alpha(gc0.get_alpha() * rgbFace[3])
rgbFace = rgbFace[:3]
gc0.set_antialiased(antialiaseds[i % Naa])
if Nurls:
gc0.set_url(urls[i % Nurls])
Expand Down Expand Up @@ -562,7 +555,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):

path, transform = self._get_text_path_transform(
x, y, s, prop, angle, ismath)
color = gc.get_rgb()[:3]
color = gc.get_rgb()

gc.set_linewidth(0.0)
self.draw_path(gc, path, transform, rgbFace=color)
1E79 Expand Down Expand Up @@ -702,7 +695,8 @@ def __init__(self):
self._joinstyle = 'round'
self._linestyle = 'solid'
self._linewidth = 1
self._rgb = (0.0, 0.0, 0.0)
self._rgb = (0.0, 0.0, 0.0, 1.0)
self._orig_color = (0.0, 0.0, 0.0, 1.0)
self._hatch = None
self._url = None
self._gi F438 d = None
Expand All @@ -711,6 +705,7 @@ def __init__(self):
def copy_properties(self, gc):
'Copy properties from gc to self'
self._alpha = gc._alpha
self._forced_alpha = gc._forced_alpha
self._antialiased = gc._antialiased
self._capstyle = gc._capstyle
self._cliprect = gc._cliprect
Expand All @@ -720,6 +715,7 @@ def copy_properties(self, gc):
self._linestyle = gc._linestyle
self._linewidth = gc._linewidth
self._rgb = gc._rgb
self._orig_color = gc._orig_color
self._hatch = gc._hatch
self._url = gc._url
self._gid = gc._gid
Expand Down Expand Up @@ -781,6 +777,13 @@ def get_dashes(self):
"""
return self._dashes

def get_forced_alpha(self):
"""
Return whether the value given by get_alpha() should be used to
override any other alpha-channel values.
"""
return self._forced_alpha

def get_joinstyle(self):
"""
Return the line join style as one of ('miter', 'round', 'bevel')
Expand Down Expand Up @@ -833,14 +836,19 @@ def get_snap(self):

def set_alpha(self, alpha):
"""
Set the alpha value used for blending - not supported on
all backends
Set the alpha value used for blending - not supported on all backends.
If ``alpha=None`` (the default), the alpha components of the
foreground and fill colors will be used to set their respective
transparencies (where applicable); otherwise, ``alpha`` will override
them.
"""
if alpha is not None:
self._alpha = alpha
self._forced_alpha = True
else:
self._alpha = 1.0
self._forced_alpha = False
self.set_foreground(self._orig_color)

def set_antialiased(self, b):
"""
Expand Down Expand Up @@ -890,30 +898,28 @@ def set_dashes(self, dash_offset, dash_list):
"""
self._dashes = dash_offset, dash_list

def set_foreground(self, fg, isRGB=False):
def set_foreground(self, fg, isRGBA=False):
"""
Set the foreground color. fg can be a MATLAB format string, a
html hex color string, an rgb or rgba unit tuple, or a float between 0
and 1. In the latter case, grayscale is used.

If you know fg is rgb or rgba, set ``isRGB=True`` for
efficiency.
If you know fg is rgba, set ``isRGBA=True`` for efficiency.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is subtle, but I think it will break the non forced alpha case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should calling set_alpha turn on the forced_alpha value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_alpha does turn on _force_alpha -- in fact, it already did, I didn't need to change anything there. (But prior these changes, practically nothing actually paid attention to it.) Calling gc.set_alpha(None) clears _forced_alpha.

This doesn't break the non-forced alpha case because the backends themselves, where appropriate, now pull the alpha value straight out of _rgb[3] in the non forced alpha case.

"""
if isRGB:
self._orig_color = fg
if self._forced_alpha:
self._rgb = colors.colorConverter.to_rgba(fg, self._alpha)
elif isRGBA:
self._rgb = fg
else:
self._rgb = colors.colorConverter.to_rgba(fg)
if len(self._rgb) == 4 and not self._forced_alpha:
self.set_alpha(self._rgb[3])
# Use set_alpha method here so that subclasses will
# be calling their own version, which may set their
# own attributes.

def set_graylevel(self, frac):
"""
Set the foreground color to be a gray level with *frac*
"""
self._rgb = (frac, frac, frac)
self._orig_color = frac
self._rgb = (frac, frac, frac, self._alpha)

def set_joinstyle(self, js):
"""
Expand Down Expand Up @@ -942,7 +948,7 @@ def set_linestyle(self, style):
'dotted' : (0, (1.0, 3.0)),
"""

if style in self.dashd.keys():
if style in self.dashd:
offset, dashes = self.dashd[style]
elif isinstance(style, tuple):
offset, dashes = style
Expand Down
17 changes: 10 additions & 7 deletions lib/matplotlib/backends/backend_cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ def set_width_height(self, width, height):
# font transform?


def _fill_and_stroke (self, ctx, fill_c, alpha):
def _fill_and_stroke (self, ctx, fill_c, alpha, alpha_overrides):
if fill_c is not None:
ctx.save()
if len(fill_c) == 3:
if len(fill_c) == 3 or alpha_overrides:
ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha)
else:
ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha*fill_c[3])
ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], fill_c[3])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch. We don't have cairo tests to check this either (we need to think about testing the cairo backend more). Nice spot.

ctx.fill_preserve()
ctx.restore()
ctx.stroke()
Expand Down Expand Up @@ -150,7 +150,7 @@ def draw_path(self, gc, path, transform, rgbFace=None):
ctx.new_path()
self.convert_path(ctx, path, transform)

self._fill_and_stroke(ctx, rgbFace, gc.get_alpha())
self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

def draw_image(self, gc, x, y, im):
# bbox - not currently used
Expand Down Expand Up @@ -316,7 +316,10 @@ def set_alpha(self, alpha):
GraphicsContextBase.set_alpha(self, alpha)
_alpha = self.get_alpha()
rgb = self._rgb
self.ctx.set_source_rgba (rgb[0], rgb[1], rgb[2], _alpha)
if self.get_forced_alpha():
self.ctx.set_source_rgba (rgb[0], rgb[1], rgb[2], _alpha)
else:
self.ctx.set_source_rgba (rgb[0], rgb[1], rgb[2], rgb[3])


#def set_antialiased(self, b):
Expand Down Expand Up @@ -359,8 +362,8 @@ def set_dashes(self, offset, dashes):
self.renderer.points_to_pixels (np.asarray(dashes)), offset)


def set_foreground(self, fg, isRGB=None):
GraphicsContextBase.set_foreground(self, fg, isRGB)
def set_foreground(self, fg, isRGBA=None):
GraphicsContextBase.set_foreground(self, fg, isRGBA)
if len(self._rgb) == 3:
self.ctx.set_source_rgb(*self._rgb)
else:
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_gdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393, F438 8 @@ def set_dashes(self, dash_offset, dash_list):
self.gdkGC.line_style = gdk.LINE_ON_OFF_DASH


def set_foreground(self, fg, isRGB=False):
GraphicsContextBase.set_foreground(self, fg, isRGB)
def set_foreground(self, fg, isRGBA=False):
GraphicsContextBase.set_foreground(self, fg, isRGBA)
self.gdkGC.foreground = self.rgb_to_gdk_color(self.get_rgb())


Expand Down
14 changes: 8 additions & 6 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ def set_width_height (self, width, height):

def draw_path(self, gc, path, transform, rgbFace=None):
if rgbFace is not None:
rgbFace = tuple(rgbFace[:3])
rgbFace = tuple(rgbFace)
linewidth = gc.get_linewidth()
gc.draw_path(path, transform, linewidth, rgbFace)

def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
if rgbFace is not None:
rgbFace = tuple(rgbFace[:3])
rgbFace = tuple(rgbFace)
linewidth = gc.get_linewidth()
gc.draw_markers(marker_path, marker_trans, path, trans, linewidth, rgbFace)

Expand Down Expand Up @@ -183,12 +183,14 @@ def __init__(self):
def set_alpha(self, alpha):
GraphicsContextBase.set_alpha(self, alpha)
_alpha = self.get_alpha()
_macosx.GraphicsContext.set_alpha(self, _alpha)
_macosx.GraphicsContext.set_alpha(self, _alpha, self.get_forced_alpha())
rgb = self.get_rgb()
_macosx.GraphicsContext.set_foreground(self, rgb)

def set_foreground(self, fg, isRGB=False):
GraphicsContextBase.set_foreground(self, fg, isRGB)
def set_foreground(self, fg, isRGBA=False):
GraphicsContextBase.set_foreground(self, fg, isRGBA)
rgb = self.get_rgb()
_macosx.GraphicsContext.set_foreground(self, rgb[:3])
_macosx.GraphicsContext.set_foreground(self, rgb)

def set_graylevel(self, fg):
GraphicsContextBase.set_graylevel(self, fg)
Expand Down
20 changes: 16 additions & 4 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ def alphaState(self, alpha):
self.nextAlphaState += 1
self.alphaStates[alpha] = \
(name, { 'Type': Name('ExtGState'),
'CA': alpha, 'ca': alpha })
'CA': alpha[0], 'ca': alpha[1] })
return name

def hatchPattern(self, hatch_style):
Expand Down Expand Up @@ -1443,11 +1443,21 @@ def check_gc(self, gc, fillcolor=None):
orig_fill = gc._fillcolor
gc._fillcolor = fillcolor

orig_alphas = gc._effective_alphas

if gc._forced_alpha:
gc._effective_alphas = (gc._alpha, gc._alpha)
elif fillcolor is None or len(fillcolor) < 4:
gc._effective_alphas = (gc._rgb[3], 1.0)
else:
gc._effective_alphas = (gc._rgb[3], fillcolor[3])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your effective alphas approach here is, IMHO, something that could be done in the GC object. The GC could then just draw an RGBA of fill and an RGBA of line color _and the backend wouldn't need to know anything about forced_alpha.

Do you know of a reason why that wouldn't be a viable approach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what I'm suggesting is that we updated the necessary signatures (to draw_path for example) such that we pass through an RGBA rather than an RGB and expect the backend to figure out the final face RGBA color.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That actually is how draw_path is working, now, but I couldn't (or rather, didn't) guarantee that everything else worked that way.

The issue is there are instances where the GC is applying an alpha property to things that have neither fill or draw colours -- e.g., images -- which is done by setting an alpha on the backend's underlying context. (The Mac OS X backend comes to mind as an example.) Supporting that means:

  • The GC still needs to make use of its _alpha in some situations
  • We don't want this to stack on top of the RGBA specified in the colors
  • So we ought to guard against that.

I believe for draw_path itself, this isn't needed because the other changes I've made mean that gc.set_alpha isn't called for that -- the alphas (overridden or not) go directly in the edge and fill colours. But there are many other draw methods that I haven't vetted that could possibly call gc.set_alpha() and give it colours with alpha channel values and I was concerned about them clashing.


delta = self.gc.delta(gc)
if delta: self.file.output(*delta)

# Restore gc to avoid unwanted side effects
gc._fillcolor = orig_fill
gc._effective_alphas = orig_alphas

def tex_font_mapping(self, texfont):
if self.tex_font_map is None:
Expand Down Expand Up @@ -2004,6 +2014,7 @@ class GraphicsContextPdf(GraphicsContextBase):
def __init__(self, file):
GraphicsContextBase.__init__(self)
self._fillcolor = (0.0, 0.0, 0.0)
self._effective_alphas = (1.0, 1.0)
self.file = file
self.parent = None

Expand Down Expand Up @@ -2072,8 +2083,8 @@ def dash_cmd(self, dashes):
offset = 0
return [list(dash), offset, Op.setdash]

def alpha_cmd(self, alpha):
name = self.file.alphaState(alpha)
def alpha_cmd(self, alpha, forced, effective_alphas):
name = self.file.alphaState(effective_alphas)
return [name, Op.setgstate]

def hatch_cmd(self, hatch):
Expand Down Expand Up @@ -2138,7 +2149,7 @@ def clip_cmd(self, cliprect, clippath):

commands = (
(('_cliprect', '_clippath'), clip_cmd), # must come first since may pop
(('_alpha',), alpha_cmd),
(('_alpha', '_forced_alpha', '_effective_alphas'), alpha_cmd),
(('_capstyle',), capstyle_cmd),
(('_fillcolor',), fillcolor_cmd),
(('_joinstyle',), joinstyle_cmd),
Expand Down Expand Up @@ -2183,6 +2194,7 @@ def copy_properties(self, other):
"""
GraphicsContextBase.copy_properties(self, other)
self._fillcolor = other._fillcolor
self._effective_alphas = other._effective_alphas

def finalize(self):
"""
Expand Down
Loading
0