Skip to content
This repository

Add gradient support #9

Open
fryguybob opened this Issue · 18 comments

4 participants

Ryan Yates Jeffrey Rosenbluth Daniel Bergey Brent Yorgey
Ryan Yates
Owner

(Imported from http://code.google.com/p/diagrams/issues/detail?id=21. Original issue from byor...@gmail.com on April 9, 2011, 12:10:27 AM UTC)

Many backends support gradients of various types. A module for describing gradients should be added to the standard library along with functions for setting the gradient attribute of a diagram.

Jeffrey Rosenbluth

I'd like to begin working on adding gradients to diagrams, I don't know if it will be ready by 1.0, but who knows. We have a number of design decisions to make before I start. Over the next view days I'll post some options to get a discussion started. If anyone knows of anyone previous discussion please let me know.

Ryan Yates
Owner
Jeffrey Rosenbluth

As I see it the first bridge to cross is deciding between the following 2 options.

  1. Create new attributes for FillGradient, LineGradient, and eventually; FillPattern, and LinePattern. The disadvantage here is that we can't use the Last semigroup to help with things like d # fc red # fillGradient g.

  2. Expand the SomeColor type to something like Color | Gradient | Pattern. I'm not sure this is possible as I haven't tried to do it yet, but if it is it will save us from dealing with situations like above where a diagram can't have both a fill color and a gradient. The disadvantage is that even if it's doable, it may break existing code.

Daniel Bergey
Owner

I take it the Gradient type includes a color, possibly two colors?

I think user code that doesn't use gradients should still be able to write # fc red. I don't want to write # fill (Solid red). Either implementation could support that, I think; this way we don't break user code.

(2) will clearly break all the Backends. (1) also has clear behavior for Backends which do not support gradients. If you go with (2), I'd like documentation of how such Backends should fall back to solid color. For these reasons, I have a slight preference for (1).

Jeffrey Rosenbluth

Yes the Gradient type should include a list of stops and colors plus several other fields.

If we go with 1. we need to come up with a way to handle the case where an instance of type class HasStyle could contain both FillColor and FillGradient attributes. I don't think leaving it up to the backends to sort this out is a good idea. One way to handle this is to insure that this never happens. This could be accomplished by augmenting the data structure of attributes to carry a list of "similar attributes" so that we can check for example if a style has a FillColor attribute before adding a FillGradient attribute if FillColor is in FillGradient's similar attributes list. This is quite an invasive change however as all attribute semigroup definitions would need to change as will as the Style semigroup.

The advantage of 2. is this case could not occur. On the other hand gradients should be Transformable, but FillColor is not, this is a further complication to option 2, although I don't think making FillColor transformable where transformations have no effect is so bad.

In the end I think 2. is the cleaner solution, but as bergey points out it will break the backends.

Jeffrey Rosenbluth

The gradient data structure should look something like this

type GradientStop = (SomeColor, Double)
data SpreadMethod = GradPad | GradReflect | GradRepeat

data LinearGradient p  = LinearGradient { _lGradStops        :: [GradientStop]
                                        , _lGradStart        :: p
                                        , _lGradSEnd         :: p
                                        , _lGradSpreadMethod :: SpreadMethod }

data RadialGradient p  = RadialGradient { _rGradStops        :: [GradientStop]
                                        , _rGradRadius       :: Double
                                        , _rGradCenter       :: p
                                        , _rGradFocus        :: p
                                        , _rGradSpreadMethod :: SpreadMethod }

basically following SVG.

Jeffrey Rosenbluth

I think option 2 would go something like this where I have begun to implement linear and radial gradients for stroking.
Filling would be similar. Then the backends would have to handle a LineTexture attribute and deal with the 3 cases.

data SomeColor = forall c. Color c => SomeColor c

type GradientStop = (SomeColor, Double)

data SpreadMethod = GradPad | GradReflect | GradRepeat

-- | Linear Gradient
data LGradient = forall p. AffineSpace p => LGradient
    { _lGradStops        :: [GradientStop]
    , _lGradStart        :: p
    , _lGradSEnd         :: p
    , _lGradSpreadMethod :: SpreadMethod }

makeLenses ''LGradient

-- | Radial Gradient
data RGradient = forall p. AffineSpace p => RGradient
    { _rGradStops        :: [GradientStop]
    , _rGradRadius       :: Double
    , _rGradCenter       :: p
    , _rGradFocus        :: p
    , _rGradSpreadMethod :: SpreadMethod }

makeLenses ''RGradient

data Texture = SC SomeColor | LG LGradient | RG RGradient
  deriving Typeable

newtype LineTexture = LineTexture (Last Texture)
  deriving (Typeable, Semigroup)
instance AttributeClass LineTexture

instance Default LineTexture where
    def = LineTexture (Last (SC (SomeColor (black :: Colour Double))))

getLineTexture :: LineTexture -> Texture
getLineTexture (LineTexture (Last t)) = t

lineTexture :: HasStyle a => Texture -> a -> a
lineTexture = applyAttr . LineTexture . Last

lineColor :: (Color c, HasStyle a) => c -> a -> a
lineColor c = lineTexture (SC (SomeColor c))
Daniel Bergey
Owner

I think your earlier version, polymorphic over the point type (or better, the vector space), was correct. Can you actually write a Transformable instance for RGradient with the forall?

I think it's simple enough to specify Backend behavior when there's both a FillColor and a Gradient. I'd expect: use the fc if there is no Gradient or if Gradient is Solid. Otherwise use the Gradient. Backends that don't support gradients can use FillColor always, just as they do now.

One downside is that to override a gradient we'll need to write # solid # fc newColor instead of just # fc newColor. Maybe that's annoying enough that we should roll FillColor into the Gradient type, instead. That means backends that cannot actually render gradients need to handle the gradient constructors anyway. Should they take the color from the first GradientStop?

Jeffrey Rosenbluth

The code above actually compiles when I drop it into Attributes.hs so I suspect you can write a Transformable instance with forall.

I guess letting the backends handle FillColor and Gradient is a reasonable choice, and you do make a good point about overriding gradients.

I'd like to hear what others think as well.

Brent Yorgey
Owner

I agree with @bergey, data LGradient = forall p. AffineSpace p ... does not seem right. In particular that actually corresponds to an existentially quantified p. So if you have an LGradient and you project out e.g. the _lGradStart, it has some particular type but you have no idea which, and the only thing you can do with it is perform AffineSpace operations. It certainly compiles but it does not have the right semantics.

Jeffrey Rosenbluth

hmm. The intent was for p to be existentially quantified. I'm not sure AffineSpace is enough but, we will only need a very limited set of operations on p. For example calculating the direction of the gradient _lGradEnd .-. lGradStart.

Brent Yorgey
Owner

OK, maybe I just don't understand the semantics of LGradient (and RGradient). I will go read about gradients in SVG before commenting further.

Brent Yorgey
Owner

OK, I have read up on gradients and I understand a bit better now. I am still not convinced about the existential quantification. What is its purpose? Even if you do lGradEnd .-. lGradStart you end up with something of type Diff p but you still don't know what p is, so you cannot use it at all.

I would expect something like

data LGradient v = LGradient
    { _lGradStops        :: [GradientStop]
    , _lGradStart        :: Point v
    ...

That is, the type of a gradient has to tell you what vector space it lives in. Otherwise how would you know what its relationship is with other things that live in some vector space?

(Also, I have intentionally not given any thought to the questions about how to structure things with gradients and fill colors, breaking backends, etc.; I have enough things to think about at the moment, it doesn't seem pressing, and I trust the rest of you to think it through and come up with something reasonable.)

Jeffrey Rosenbluth

Ah, I see where I went wrong.
What about the issue of using something like Texture to be the attribute, so that the semigroup handles sorting out FillColor vs LGradient and the backends have to handle Texture or leaving FillColor intact and creating a new gradient attribute and letting the backends sort out FillColor vs LGradient?

Oh, I didn't see the parenthetical to you comment, go ahead and disregard my last question?

Jeffrey Rosenbluth

@bergey Hard to say where the backends should take their color from is they don't support gradients. Depending on where the user assumes the light is coming from any of the stops could make the most sense. Perhaps the backend should just ignore gradients and us the default color - i.e. clear.

Jeffrey Rosenbluth

Adding gradients for strokes might look something like this for R2.
1) I'm not sure if this can be generalized for an arbitrary vector space v
2) If we only implement gradients for R2, this code should probably be in TwoD. However it would be very strange to put the LineColor attribute which does not depend on a vector space in TwoD. Also gradients should be transformable and colors are not so making them transformable to be part of a Texture attribute is a bit of a hack.
3) I'm thinking if we cant generalize this code to arbitrary v perhaps gradients should be a separate attribute.

thoughts?

class Color c where
  -- | Convert a color to its standard representation, AlphaColour
  toAlphaColour :: c -> AlphaColour Double

-- | An existential wrapper for instances of the 'Color' class.
data SomeColor = forall c. Color c => SomeColor c

type GradientStop = (SomeColor, Double)

data SpreadMethod = GradPad | GradReflect | GradRepeat

-- | Linear Gradient
data LGradient = LGradient
    { _lGradStops        :: [GradientStop]
    , _lGradStart        :: P2
    , _lGradSEnd         :: P2
    , _lGradSpreadMethod :: SpreadMethod }

makeLenses ''LGradient

-- | Radial Gradient
data RGradient = RGradient
    { _rGradStops        :: [GradientStop]
    , _rGradRadius       :: Double
    , _rGradCenter       :: P2
    , _rGradFocus        :: P2
    , _rGradSpreadMethod :: SpreadMethod }

makeLenses ''RGradient

data Texture = SC SomeColor | LG LGradient | RG RGradient
  deriving (Typeable)

newtype LineTexture = LineTexture (Last Texture)
  deriving (Typeable, Semigroup)
instance AttributeClass LineTexture

type instance V LineTexture = R2

instance Transformable LineTexture where
  transform t l@(LineTexture (Last (SC _))) = l
  -- XXX Need to handle linear and radial grandient transforms.
  transform t l@(LineTexture lt) = l

instance Default LineTexture where
    def = LineTexture (Last (SC (SomeColor (black :: Colour Double))))

getLineTexture :: LineTexture -> Texture
getLineTexture (LineTexture (Last t)) = t

lineTexture :: (HasStyle a, V a ~ R2) => Texture-> a -> a
lineTexture = applyTAttr . LineTexture . Last

lineColor :: (Color c, HasStyle a, V a ~ R2) => c -> a -> a
lineColor c = lineTexture (SC (SomeColor c))

-- | A synonym for 'lineColor', specialized to @'Colour' Double@
--   (i.e. opaque colors).
lc :: (HasStyle a, V a ~ R2) => Colour Double -> a -> a
lc = lineColor

-- | A synonym for 'lineColor', specialized to @'AlphaColour' Double@
--   (i.e. colors with transparency).
lcA :: (HasStyle a, V a ~ R2) => AlphaColour Double -> a -> a
lcA = lineColor

lineLGradient :: (HasStyle a, V a ~ R2) => LGradient -> a -> a
lineLGradient g = lineTexture (LG g)

lineRGradient :: (HasStyle a, V a ~ R2) => RGradient -> a -> a
lineRGradient g = lineTexture (RG g)
Daniel Bergey
Owner

I think you're right that the sort of textures and patterns we'd want to support in 3D are quite different from these. But the plan for 3D is to not use FillColor, and instead use new attributes that make sense under lighting (eg, diffuse, specular). So the situation I worried about where 3D backends have no plan to support gradients but need to handle gradient constructors isn't really a problem.

Daniel Bergey bergey referenced this issue
Open

Gradient #136

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.