# Difference between revisions of "Advanced Gradients"

Line 45: | Line 45: | ||

One reasonably obvious way to mimic a conical gradient is to use a circular gradient to define a conical surface and then use feDiffuseLighting. This doesn't directly result in the right mapping, but it at least shares the property that the pixels on radial lines from the center have the same value. In addition, the generated gradient will be symmetrical, so if this is not wanted the region should be split in two along the symmetry axis. | One reasonably obvious way to mimic a conical gradient is to use a circular gradient to define a conical surface and then use feDiffuseLighting. This doesn't directly result in the right mapping, but it at least shares the property that the pixels on radial lines from the center have the same value. In addition, the generated gradient will be symmetrical, so if this is not wanted the region should be split in two along the symmetry axis. | ||

To derive the right parameters, first define the (ideal) surface of the cone created by the gradient as <math>Z(x,y)=s-\frac{s}{r}\sqrt{x^2+y^2}</math> (without loss of generality the center is supposed to be at <math>(0,0)</math>). Per the SVG 1.1 specification the surface normal is defined by using a sobel filter, however, as this is a reasonable approximation of a directional derivative we use the derivative here instead. Specifically, taking the right scale factors into account, the surface normal is defined as: | |||

<math>\begin{align} | |||

N'_x(x,y)&=-2 s \frac{\partial}{\partial x}Z(x,y) | |||

\\&=\frac{2sx}{r\sqrt{x^2+y^2}} | |||

\\N'_y(x,y)&=\frac{2sy}{r\sqrt{x^2+y^2}} | |||

\\N'_z(x,y)&=1 | |||

\\N(x,y)&=\frac{N'}{\|N'\|} | |||

\end{align}</math> | |||

Assuming a distant (white) light source with a zero azimuth and a diffuse lighting constant of one, the resulting intensity can be computed as: | |||

<math>\begin{align} | |||

D(x,y)&=N\cdot L | |||

\\&=\frac{1}{\|N'\|}\left(\frac{2sx\cos(e)}{r\sqrt{x^2+y^2}} + \sin(e)\right) | |||

\\&=\frac{1}{\sqrt{4s^2x^2+4s^2y^2+r^2\sqrt{x^2+y^2}^2}}\left(2sx\cos(e) + \sin(e)r\sqrt{x^2+y^2}\right) | |||

\\&=\frac{1}{\sqrt{(4s^2+r^2)(x^2+y^2)}}\left(2sx\cos(e) + \sin(e)r\sqrt{x^2+y^2}\right) | |||

\end{align}</math> | |||

To get a good, usable, gradient <math>D(x,0)</math> should be 1 for positive <math>x</math> and 0 for negative <math>x</math>: | |||

<math>\begin{align} | |||

D(x,0)&=\frac{1}{|x|\sqrt{4s^2+r^2}}\left(2sx\cos(e) + \sin(e)r|x|\right) | |||

\\&=\frac{1}{\sqrt{4s^2+r^2}}\left(2s\mbox{sign}(x)\cos(e) + \sin(e)r\right) | |||

\\D(x_-,0)&=0 | |||

\\0&=\frac{1}{\sqrt{4s^2+r^2}}\left(-2s\cos(e) + \sin(e)r\right) | |||

\\2s\cos(e)&=\sin(e)r | |||

\\\frac{\sin(e)}{\cos(e)}&=\frac{2s}{r} | |||

\\e&=\arctan(\frac{2s}{r}) | |||

\\D(x_+,0)&=1 | |||

\\1&=\frac{1}{\sqrt{4s^2+r^2}}\left(2s\cos(e) + \sin(e)r\right) | |||

\\\sqrt{4s^2+r^2}&=2\sin(e)r | |||

\\\sqrt{4s^2+r^2}&=\frac{4rs}{\sqrt{4s^2+r^2}} | |||

\\4s^2+r^2&=4rs | |||

\\4s^2-4rs+r^2&=0 | |||

\\(2s-r)^2&=0 | |||

\\s&=\frac{1}{2}r | |||

\\e&=\arctan(\frac{2s}{2s}) | |||

\\&=\frac{1}{4}\pi | |||

\end{align}</math> | |||

So, surfaceScale should be set to half of the radius of the radial gradient and the elevation should be 45 degrees. This yields the following (after filling in and simplifying), which is simply the cosine of the angle we would like (scaled and translated so that it fits in the range <math>[0,1]</math>): | |||

<math>D(x,y)=\frac{x}{2\sqrt{x^2+y^2}} + \frac{1}{2}</math> | |||

For a perfect result we should now correct for the fact that we have a cosine where we would want an angle, but in many cases this is probably not necessary. | |||

==Spiral gradient== | ==Spiral gradient== |

## Revision as of 14:17, 2 April 2009

There are (apparently) several types of gradients that can be interesting to support, but are not part of the SVG standard. This page is meant to document which gradient types Inkscape may want to support and how these could be simulated.

## Separating shape/pattern from color

Any gradient can be defined by a mapping [math]\displaystyle{ f:\mathbb{R}^2\rightarrow[0,1] }[/math] from any point in the plane to a value in the range [math]\displaystyle{ [0,1] }[/math], combined with a mapping from the range [math]\displaystyle{ [0,1] }[/math] to colors. The former is defined by the type of gradient (and its parameters) and the latter by the gradient stops.

For each new gradient type it suffices to find a good method of generating [math]\displaystyle{ f }[/math] (over all four color channels). The desired colors can then be determined by using an feComponentTransfer filter with the table transfer function type. This allows the gradient to be approximated to any required accuracy. An example for a gradient from red to green to blue to transparent black (assumes we are in a filter definition and an image with [math]\displaystyle{ f }[/math] on all four channels is in "f"):

<feComponentTransfer in="f"> <feFuncR type="table" tableValues="1 0 0 0" /> <feFuncG type="table" tableValues="0 1 0 0" /> <feFuncB type="table" tableValues="0 0 1 0" /> <feFuncA type="table" tableValues="1 1 1 0" /> </feComponentTransfer>

Alternatively the mapping to colors of the gradient could be done using feDisplacementMap, if [math]\displaystyle{ f }[/math] is pre-processed properly. Specifically, the values of [math]\displaystyle{ f }[/math] are meant to be interpreted in an absolute sense, while feDisplacementMap uses them as an offset relative to the current position. So if [math]\displaystyle{ (x1,y1),(x2,y2) }[/math] defines the bounding box of the object to which the gradient is applied, and a (horizontal) linear gradient is applied to a rectangle using these same coordinates, then the scale parameter should be set in such a way that [math]\displaystyle{ x1+scale*0.5=x2 }[/math] and [math]\displaystyle{ x2-scale*0.5=x1 }[/math], so [math]\displaystyle{ scale=2(x2-x1) }[/math]. The x displacement channel should then be set to a channel corresponding to the pre-processed [math]\displaystyle{ f }[/math] and the y displacement channel should then be set to some channel that is 0.5 everywhere. An example for the same gradient as above (we are in a defs element, the red channel is assumed to correspond to [math]\displaystyle{ f }[/math] and the green channel to one, the bounding box is assumed to be [math]\displaystyle{ (0,0),(1,1) }[/math]):

<g id="symbolstops"> <linearGradient id="gradientstops"> <stop offset="0" stop-color="#f00" /> <stop offset="0.33" stop-color="#0f0" /> <stop offset="0.67" stop-color="#00f" /> <stop offset="1" stop-color="#000" stop-opacity="0" /> </linearGradient> <rect width="150" height="150" fill="url(#gradientstops)" /> </g> <g id="symbolxoffset"> <linearGradient id="gradientxoffset"> <stop offset="0" stop-color="#F00" /> <stop offset="1" stop-color="#000" /> </linearGradient> <rect width="1" height="1" fill="url(#gradientxoffset)" /> </g> <filter ...> ... <feImage xlink:href="#symbolxoffset" result="xoffset" /> <feComposite in="f" in2="xoffset" result="fp" operator="arithmetic" k2="0.5" k3="0.5" /> <feImage xlink:href="#symbolstops" result="stops" /> <feDisplacementMap in="stops" in2="fp" scale="2" xChannelSelector="R" yChannelSelector="G" /> </filter>

This last method has the advantage that there is a more direct correspondence with the gradient stops defined by the user (and does not use an approximation to the gradient as the feComponentTransfer method), but it is obviously also more complex. Also, if the mapping [math]\displaystyle{ f }[/math] needs a (non-linear) correction this can directly be taken into account in the feComponentTransfer method. However, the feComponentTransfer method can give a lower quality result if the alpha channel is also part of the gradient (and the viewer uses premultiplied alpha).

## Conical gradient

"A gradient which goes along the circular arc around a center." In other words (ignoring rotations and so on), given a center [math]\displaystyle{ (cx,cy) }[/math], the mapping [math]\displaystyle{ f }[/math] is defined as [math]\displaystyle{ f(x,y)=\frac{1}{2\pi}\arctan(y-cy,x-cx)+\frac{1}{2} }[/math].

One reasonably obvious way to mimic a conical gradient is to use a circular gradient to define a conical surface and then use feDiffuseLighting. This doesn't directly result in the right mapping, but it at least shares the property that the pixels on radial lines from the center have the same value. In addition, the generated gradient will be symmetrical, so if this is not wanted the region should be split in two along the symmetry axis.

To derive the right parameters, first define the (ideal) surface of the cone created by the gradient as [math]\displaystyle{ Z(x,y)=s-\frac{s}{r}\sqrt{x^2+y^2} }[/math] (without loss of generality the center is supposed to be at [math]\displaystyle{ (0,0) }[/math]). Per the SVG 1.1 specification the surface normal is defined by using a sobel filter, however, as this is a reasonable approximation of a directional derivative we use the derivative here instead. Specifically, taking the right scale factors into account, the surface normal is defined as:

[math]\displaystyle{ \begin{align} N'_x(x,y)&=-2 s \frac{\partial}{\partial x}Z(x,y) \\&=\frac{2sx}{r\sqrt{x^2+y^2}} \\N'_y(x,y)&=\frac{2sy}{r\sqrt{x^2+y^2}} \\N'_z(x,y)&=1 \\N(x,y)&=\frac{N'}{\|N'\|} \end{align} }[/math]

Assuming a distant (white) light source with a zero azimuth and a diffuse lighting constant of one, the resulting intensity can be computed as:

[math]\displaystyle{ \begin{align} D(x,y)&=N\cdot L \\&=\frac{1}{\|N'\|}\left(\frac{2sx\cos(e)}{r\sqrt{x^2+y^2}} + \sin(e)\right) \\&=\frac{1}{\sqrt{4s^2x^2+4s^2y^2+r^2\sqrt{x^2+y^2}^2}}\left(2sx\cos(e) + \sin(e)r\sqrt{x^2+y^2}\right) \\&=\frac{1}{\sqrt{(4s^2+r^2)(x^2+y^2)}}\left(2sx\cos(e) + \sin(e)r\sqrt{x^2+y^2}\right) \end{align} }[/math]

To get a good, usable, gradient [math]\displaystyle{ D(x,0) }[/math] should be 1 for positive [math]\displaystyle{ x }[/math] and 0 for negative [math]\displaystyle{ x }[/math]:

[math]\displaystyle{ \begin{align} D(x,0)&=\frac{1}{|x|\sqrt{4s^2+r^2}}\left(2sx\cos(e) + \sin(e)r|x|\right) \\&=\frac{1}{\sqrt{4s^2+r^2}}\left(2s\mbox{sign}(x)\cos(e) + \sin(e)r\right) \\D(x_-,0)&=0 \\0&=\frac{1}{\sqrt{4s^2+r^2}}\left(-2s\cos(e) + \sin(e)r\right) \\2s\cos(e)&=\sin(e)r \\\frac{\sin(e)}{\cos(e)}&=\frac{2s}{r} \\e&=\arctan(\frac{2s}{r}) \\D(x_+,0)&=1 \\1&=\frac{1}{\sqrt{4s^2+r^2}}\left(2s\cos(e) + \sin(e)r\right) \\\sqrt{4s^2+r^2}&=2\sin(e)r \\\sqrt{4s^2+r^2}&=\frac{4rs}{\sqrt{4s^2+r^2}} \\4s^2+r^2&=4rs \\4s^2-4rs+r^2&=0 \\(2s-r)^2&=0 \\s&=\frac{1}{2}r \\e&=\arctan(\frac{2s}{2s}) \\&=\frac{1}{4}\pi \end{align} }[/math]

So, surfaceScale should be set to half of the radius of the radial gradient and the elevation should be 45 degrees. This yields the following (after filling in and simplifying), which is simply the cosine of the angle we would like (scaled and translated so that it fits in the range [math]\displaystyle{ [0,1] }[/math]):

[math]\displaystyle{ D(x,y)=\frac{x}{2\sqrt{x^2+y^2}} + \frac{1}{2} }[/math]

For a perfect result we should now correct for the fact that we have a cosine where we would want an angle, but in many cases this is probably not necessary.

## Spiral gradient

Here defined as similar to a conical gradient, but with the mapping [math]\displaystyle{ f }[/math] also depending on the distance to the center. Specifically [math]\displaystyle{ f(x,y)=\frac{1}{2\pi}\arctan(y-cy,x-cx)+\sqrt{(x-cx)^2+(y-cy)^2}\mod 1 }[/math].

If a conical gradient can be created then this can be created by simply adding the distance to the center. If needed use can be made of the fact that modular arithmetic is used (that is, a long steep gradient is equivalent to many small, equally steep, gradients).

To avoid overflow in the mapping it can be scaled by 0.5, allowing two "periods" in the [0,1] range. In this case the mapping to colors should be changed accordingly.

## More

Diffusion curves? See [[1]].