googlefonts / colr-gradients-spec

63 stars 8 forks source link

Radial gradients and negative radii with variations or non-normal color stops #367

Open drott opened 2 years ago

drott commented 2 years ago

When implementing COLRv1 variations we're running into problems when varying color stops of COLRv1 PaintVarRadialGradient into the negative, or varying the radii of PaintVarRadialGradient into negative values.

Two scenarios can lead to that: 1) Simply attaching a variation axis to r0 and r1 and specifying negative values as part of the axis range. 2) Having a smaller circle A and a larger circle B, and then shifting color stops near the center of A to smaller values so that they would be outside the vector from c0 to c1. In most implementations the color stops need to normalized to the 0 to 1 range and the approach used is usually to interpolate new circle centers and radii, so that they are equivalent to the original gradient definitions, but color stops are in the range of [0, 1]. This interpolation can result in one of the two interpolated circles having a negative radius.

It seems to me like in principle this is currently well-defined in the spec - and nothing should be drawn for circles with a radius < 0.

But with this definition we run into implementation problems when the color line does not end exactly at the 0-radius circle. Since the gradient implementations expect normalized color stops, we would need to manually interpolate a color for color stop 1 or 0 at the zero-radius circle. But specifying manually a start/end interpolated color and truncating the color line then means that the shader is not aware of the full color-line and repeat modes stop working correctly after the first iteration of the color line.

Changing the shader/gradient programs seems highly difficult as they are currently optimized for CSS gradients - which by their definition of circles etc. do not run into this problem.

@jfkthame and I discussed possible solutions for this situation, and I am inviting Jonathan to suggest what a possible solution to this might look like.

jfkthame commented 2 years ago

For scenario (1), using a variation axis to alter r0 or r1, one reasonable argument would be that because radii are unsigned values (stored as UFWORD in the font table), it is impossible for them to become negative, and so the resolved value when variations are applied should simply be clamped at zero. This would avoid the question of what it means to have a circle of negative radius. And when applied to an explicit variation of the radius, authors should have no trouble understanding that the radius cannot become negative, and so variations cease to have any further effect when that value is reached.

Another argument would be that when the radius is negative, nothing at all should be rendered for the gradient -- which is what is implied by the HTML canvas spec: "If either of r0 or r1 are negative, then an "IndexSizeError" DOMException must be thrown". So when a radius varies to a negative value, the gradient becomes undefined and disappears.

If this issue were only about applying variations to the radii, I think either of these answers would be entirely reasonable, and readily implementable.

However, neither of these answers give a satisfactory result in scenario (2); and I think that as far as possible, we should aim for consistency between the two scenarios, rather than addressing them separately.

In case (2) the gradient is defined by two circles with non-negative radii, but normalization of the color line results in a "projected" starting or ending circle whose projected radius becomes negative. Note that the specification of the gradient uses perfectly valid circles; the fact that we end up with a negative projected radius is an implementation detail that should not cause a complete failure to render.

As Dominik notes, "slicing" the definition of the normalized gradient at the zero-radius position isn't a workable approach because repeat/reflect modes will fail to work correctly. And in this case, clamping the projected radius at zero, while implementable, would give a result that is difficult for authors to relate to their specification of the gradient, because of the inconsistency it would introduce between how the center position and the radius are treated.

There is, I think, another reasonable interpretation of what it means for one of the circles defining a gradient to have a negative radius: the gradient is simply rendered using the absolute values of the radii. This avoids any implementation problems of the other interpretations. And intuitively, it makes some sense. If we consider the radius of the circle as a vector of length r from the center (in any arbitrary direction), we can picture what happens as it varies towards zero: the circle shrinks. When it reaches zero, the circle collapses to a point. Continuing the variation, the radius vector begins to extend in the opposite direction from the center: and so the circle that it defines begins to grow again.

This matches the behavior I have observed in the FontGoggles testing tool, and is, I think, the most reasonable and implementable solution given the available painting APIs.

I suggest, therefore, that the COLRv1 spec should explicitly say that:

(a) When variations would cause one or both of the radii in PaintVarRadialGradient to become negative, it is resolved to the absolute value.

(b) When the color line used in Paint[Var]RadialGradient has a non-[0.0 - 1.0] range, the stop offsets are normalized to the range [0.0 - 1.0], and the center coordinates and radii are interpolated/extrapolated accordingly; if the resulting radii become negative, their absolute values are used.

PeterConstable commented 2 years ago

Re Jonathan's suggestion of using absolute value, is there a gotcha in this part of the algorithm spec:

For all values of ω where r(ω) > 0, starting with the value of ω nearest to positive infinity and ending with the value of ω nearest to negative infinity, draw the circular line with radius r(ω) centered at position (x(ω), y(ω)), with the color at ω, but only painting on the parts of the bitmap that have not yet been painted on in this step of the algorithm for earlier values of ω.

The parameter ω starting at +ve infinity could represent either a r<0 end or the opposite end, depending on how c0 and c1 are specified. But either way, once pixels are painted, they're not re-painted. That would work if painting starts at the other end away from r<0, but not if it starts at the r<0 end.

rsheeter commented 2 years ago

The intepretation of negative radius as just going the opposite direction but still producing the same circle proposed in https://github.com/googlefonts/colr-gradients-spec/issues/367#issuecomment-1227451496 makes a lot of sense to me, at least on initial reading.

rsheeter commented 2 years ago

The "hourglass" interpretation also seems defensible but iiuc it's less implementable so to me, given COLRv1 deliberately aspires to be implementation friendly, we should take the |r| interpretation.

drott commented 2 years ago

I think the change of the radius depending on the color line would need to be explicitly explained or specified, as otherwise that is suprising and not clear from the rest of the text. This is due to the extrapolation towards a zero-radius circle that can still display the smallest color stop, or in other words due to the internal implementation detail of attempting to normalize the color stops to 0,1.

image

PeterConstable commented 2 years ago

There's an odd behaviour that arises from taking the absolute value of the circle radii in combination with normalizing the colour line, interpolating circles to the normalized colour line, and then using the absolute values of the interpolated circle radii. I've crafted an HTML file that illustrates this: TestRadialGradientDefs.html.zip

If the beginning colour stop offset is < 0, we project an alternate of circ0, circ0Alt. Since circ0 is smaller than circ1, circ0Alt is even smaller. Decreasing the stop offset will, of course, eventually make the circ0Alt radius hit 0.

image

Now suppose the radius of circ0 is variable, and we start reducing its radius. Of course, that will also cause the radius of the projected circle, circ0Alt, to hit 0 and go negative even sooner.

image

If we continue to decrease the radius of circ0 but don't take the absolute value of the radius for circ0Alt, the latter will remain (<=) 0 as the circ0 radius is further decreased...

image

... until the radius circ0 has reached 0 and then the absolute value of circ0 radius gets large enough again.

But if you use the absolute value of the circ0Alt radius, then the behaviour gets weirder as the radius of circ0 continues to decrease. The radius of circ0Alt reaches 0, then immediately starts to get bigger while the radius of circ0 continues to get smaller but hasn't yet reached 0.

image

But that happens only for a short bit. When the radius of circ0 gets to 0, the radius of circ0Alt reaches a local maximum. Then it decreases again while circ0 grows because the absolute value of its radius is used. After the radius of circ0Alt reaches 0 again, it finally starts increasing monotonically as the delta for R0 gets more negative.

Repro steps using the HTML in the attached .zip:

  1. Click the checkbox to use the absolute value for the radii of interpolated circles.
  2. Set the begin stop offset to -0.4.
  3. Using the slider for R0 deltas, make the delta increasingly negative.
  4. Observe:
    • At R0.delta = -22, the radius of circ0Alt reaches 0.
    • At R0.delta = -40, the radius of circ0 reaches 0, and the radius of circ0Alt hits a local maximum.
    • At R0.delta = -58, the radius of circ0Alt has returned to 0.
    • For R0.delta decreasing below -58, the radius of circ0Alt continues to grow.

Given that odd behaviour, it might make more sense to use the absolute value of radii for circ0 and circ1, but not for the projected circles.

drott commented 2 years ago

Given that odd behaviour, it might make more sense to use the absolute value of radii for circ0 and circ1, but not for the projected circles.

I agree with you that the absolute value should be only taken once, but not of the original circle, but only for the final projected circles. In that sense, I don't think this step of the diagram reflects the intended behaviour.

image

Here, the projected circle on the left should be visible and the dotted lines indicating the filled are should be tangent to that.

PeterConstable commented 2 years ago

If the dotted lines aligned to the projected circle rather than the original circle, that would be diverging from the WHATWG createRadialGradient() algorithm spec.

jfkthame commented 2 years ago

Assuming you're referring to https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createradialgradient, I'm not really understanding how it's relevant:

context.createRadialGradient() is spec'd to throw an exception if either radius is negative; so it can't create a gradient with negative radii.

And gradient.addColorStop() is spec'd to throw an exception if a stop offset is outside the interval [0.0 .. 1.0]. So it cannot set a color stop outside of the space between the two circles, such that our "normalization" of the color line would result in a projected circle that could become negative-radius.

So (AFAICT) that spec simply doesn't address the situations we're dealing with here.

jfkthame commented 2 years ago

I've filed https://bugzilla.mozilla.org/show_bug.cgi?id=1789380 with a patch to adjust Firefox's behavior here such that it matches what Dominik has implemented in Chrome Canary.

PeterConstable commented 2 years ago

I'm trying to update my HTML page, but encountering odd behaviour given that both ends of the colour line might require projection beyond [0,1]. Is there a build of either browser that can be downloaded with the revised behaviour?

Something that puzzles me about this is that, intuitively, it seems that shifting stop offsets should not affect the shape of the cone/cylinder (that should be determined solely by the defined circles), yet this seems to entail that stop offsets < 0 or > 1 can alter the shape.

drott commented 2 years ago

Up to date FF nightly and Chrome Canary now have the same behavior, I checked this morning.

PeterConstable commented 2 years ago

I've revised my HTML page to do what we discussed—extrapolate circles c0' and c1' from c0 and c1 for a colour line with begin stop offset < 0 or end stop offset > 1, and take the absolute value of the radii for those extrapolated circles (but not for r0 or r1 if deltas make them < 0). After revising calculations, I compared with the change Jonathan made in Gecko, and the logic is the same.

TestRadialGradientDefs_Revised.zip

In this revised HTML page, I also added an extra feature: Previously, I drew lines to show the envelope cylinder/cone defined by c0 and c1. Now, in addition, I draw separate lines to show the envelope cylinder/cone defined by c0' and c1'. This highlights a concern with the proposed revision of the algorithm.

As spec'd in OT1.9, the shape of the envelope cylinder/cone is determined solely by the specification of the circles, c0 and c1. In particular, the shape is not affected by the location of stop offsets. With the proposed revision of the algorithm, that continues to hold true but only so long as the radius of the extrapolated circles remains >= 0.

Here's an example with a begin offset at -0.6 and end offset at 1.2. The circles with dotted outlines are c0 and c1; the circles with solid outlines are c0' and c1'.

image

Note that the shape of the cone obtained from c0' and c1' is the same as that obtained from c0 and c1.

Now suppose the begin stop offset were variable and animated to move to -1.3. Here's what happens:

image

Note that the cone obtained from c0' and c1' is a different shape than the cone obtained from c0 and c1.

This behaviour is visible using the test font and html page that Dominik has shared (separately in email) using the latest Firefox Nightly build (106.0a1 (2022-09-07)). In the following video clip, the COL1 variation axis of the font is manipulated to change the begin stop offset. In particular, notice what happens with the three radial gradients in the bottom line (same except for different extend modes).

https://user-images.githubusercontent.com/26614869/189007482-1f5fd8fd-9a9e-49c6-a45c-64de09aeb546.mp4

First, COL1 is moved from 0.02 to 0.36, moving the begin stop away from the tip of the cone—everything is fine. But then COL1 is manipulated to move the begin stop toward the tip of the cone. The stop reaches the tip at around -0.46, and up to that point the shape of the cone has not been affected. But beyond that the shape of the cone changes; this is when the projected radius for c0' goes negative, and its absolute value is taken.

I think this is a problem in a couple of ways. First, it hinders an effect that designers may want to use in variable fonts, having a gradient flow off the end of the cone. Secondly, unless we spec this as required behaviour, it will lead to non-interoperability of fonts between different implementations. For example, the following video clip shows basically the same scenario with the same test font, but this time in a test app using DWriteCore (Experimental release):

https://user-images.githubusercontent.com/26614869/189009081-168453ca-472e-4644-bc48-471e1a006f4a.mp4

(Note: Nick has a bug in his implementation vis-à-vis the OT1.9 COLR spec, overlooking the clause, "For all values of ω where r(ω) > 0," leading to the cone being continued beyond the tip. While it is a bug, it is helpful in showing the colour line continuing beyond the tip without the stop offset affecting the shape.)

PeterConstable commented 2 years ago

Somewhat ironically, we began discussing a year and a half ago (#148) whether there needed to be colour stops within the [0,1] range, and I asked whether we'd encounter any constraint in existing graphics libraries. The response I got was, "If any graphics library has such limitations, implementation of COLRv1 on top of that library needs to add extrapolated points as necessary." But what wasn't considered then is whether it would be ok to have the color line affect the shape of the gradient rather than just determining the colours in the gradient.

drott commented 2 years ago

If the implied suggestion in the previous comment is to restrict to [0,1] for color stops, or restrict to >= 0 for radii, I don't think that will work without somewhat counterintuitive special casing and redefinitions of how color lines are to be interpreted. Please note that constraining to a [0,1] interval in drawing was one of the reasons for the problems we've had with PaintVarSweepGradient.

We're faced with the following observations:

To define some kind of clamping then, we would end up with a somewhat complicated definition of what to constrain to, i.e. clamp to the extrapolated position where the combination of color stop offset and radius slope (between the two circles) hits 0. But then we're back to adapting the result to shader API: We would need to give the shader a manually interpolated color stop and change the color line, which leads to repeat mode problems:

I think this is a problem in a couple of ways. First, it hinders an effect that designers may want to use in variable fonts, having a gradient flow off the end of the cone. Secondly, unless we spec this as required behaviour, it will lead to non-interoperability of fonts between different implementations.

Of course the goal is to achieve tight interop, that's why we're discussing this issue.

I agree that the "geometrical" effect is undesirable, however:

PeterConstable commented 2 years ago

I wouldn't suggest any change affecting radial or (as we have in OT1.9.1 alpha) sweep gradients, neither of which have any such issues. But radial / two-point conical gradients are different because they implicitly define a shape. I've asked some other graphics people within MS and heard the same intuition: the colour line definition should not affect the shape.

PDF, HTML Canvas 2K, Skia, Cairo all support this type of gradient, and in all of them the colour line does not affect the shape. But they also impose constraints that---so far---we have not: the colour stop offsets for this type of gradient must be within [0.0, 1.0], and the circle radii must be > 0.

If we were to clamp the stop offsets for radial gradients to [0, 1] and clamp the circle radii to >=0, then I think it would provide the following:

To define some kind of clamping then, we would end up with a somewhat complicated definition of what to constrain to,

Not complicated at all: as suggest above, there is no extrapolation of circles, no interpolation of intermediate colours, and only one extra check beyond what is already spec'd: if stop offset (for a given variation instance) is < 0, set to 0; if > 1, set to 1.

Regarding the cone effect: I don't think a gradient cone should be use for glyph geometry - a PaintGlyph clipping mask should be use for that instead.

We, the format requires PaintRadialGradient to be used in a PaintGlyph fill, so PaintGlyph as a clipping mask is necessarily going to be involved. Yet the geometry is implicit in this type of gradient, just as it would be for mesh gradients. We should expect fonts will take advantage of that, e.g. having a clipping mask that doesn't mask all non-painted portions of the gradient.

But also—and more importantly—because the colour line is affecting the shape, it also affects the iso-colour contours of the gradient. Even with a PaintGlyph clipping mask, this will be noticeable, and could be frustrating for designers.

I can be convinced that the concern I raise isn't a blocker. But I want to be cautious with this since we need to get it right, and soon (we know there are more implementations on the way), and would not want to end up with something that designers forever find counterintuitive. Hence, I'd really like to see a wider range of input than just you, Jonathan and me.

jfkthame commented 2 years ago

I think we have essentially two options to resolve the issue here in a well-defined and widely-implementable way:

(a) Let the radius-variation and color-line normalization & extrapolation math do its unconstrained thing; and then if we find ourselves with negative radii -- which many shader APIs cannot handle -- we take the absolute value, as currently implemented in Firefox NIghtly & Chrome Canary. (A minor alternative would be to clamp negative radii to zero instead of taking the abs value, but there's no particular benefit to that -- the result is still a distortion of the "ideal" geometry.)

(b) Avoid allowing negative values to arise, by clamping the effect of any variations on the radii and restricting the color stop range to [0, 1] so that extrapolation of the circles is not required to normalize the stops. The gradient is then directly implementable with common shader APIs.

As noted, approach (a) has the disadvantage that in some cases it leads to an unexpected change to the shape of the gradient. But a designer can avoid this by not venturing into those areas of the color-line and radius-variation space. Essentially, if the designer respects the constraints of (b) in their definition of the gradient, instead of relying on the implementation to clamp things, the result will be the same and the gradient shape will be unaffected.

The main reservation I have about approach (b) is that it means a given color line may behave quite differently for radial gradients than the same color line used by a linear gradient. This seems to me at least as unfortunate (and unexpected for designers) as the unexpected effect of the color stop range on the shape of the radial gradient.

Consider a simple color line with three stops: [red @ 0.0, green @ 0.5, blue @ 1.0], and two gradients that share this color line: a linear gradient from (0, 500) to (1000, 500), and a radial gradient from (0, 500; r=500) to (1000, 500; r=500). These will be similar-looking gradients progressing from left to right, with the difference that the radial gradient has curved color contours while the linear gradient has straight.

However, now suppose we vary the color stop offsets by adding 1.0 to all of them. For the linear case, this simply shifts the colors within the gradient to the right (entirely intuitive). But for the radial (curved) gradient, under proposal (b) the offsets all get clamped to 1.0, and the result is -- what? Presumably the gradient collapses to a simple red/blue fill with a curved edge between them? Whereas (a) would allow the colors of the curved gradient to shift rightwards just like its linear counterpart.

Perhaps, if we choose (b) here (to avoid distorting the geometry of the radial gradient in these edge cases) we should apply the same clamping of the color stop offsets to all gradients, so that color lines behave more consistently?

PeterConstable commented 2 years ago

Consider a simple color line with three stops...

It may or may not be obvious to designers to craft a font in such a way that a ColorLine table is actually shared, but the example is valid. Note, though, that your example has the two circles with identical radius. That's a special case in which the distortion issues don't arise. If, instead the example had (0, 500; r=500) to (1000, 500; r=600). then the curved contours will start changing, which the designer might not expect.

A further critique of option (b) that had not occurred to me earlier is that, if a color stop offset were to vary to < 0 and get clamped to 0, then that will affect not just colors at one end of the gradient but also how the pattern mirrors and reflects. So, for example, if two stops toward the tip of a cone were getting compressed because both offsets are varying but one is clamped, then that compression will also occur in each repeat/mirror of the pattern.

I wouldn't want to end up clamping for all gradients: conceptually, the colour line is infinitely long, and it shouldn't matter what sub-range is used to specify its pattern. I think the radial gradient behaviour as currently spec'd is great. It's really unfortunate that some graphics implementations didn't allow for more flexibility in the colour line description. The kinds of animated variation behaviours we can provide in COLRv1 could also be desirable in other contexts.

Side note: it turns out that Direct2D's behaviour for colour lines in which the begin offset is > 0 or the end offset is < 1 is that the begin colour is padded out to 0 / end colour is padded out to 1. And if offsets are outside [0, 1], then the offset is ignored. So, if a stop were animated out of the [0,1] range, then that colour will disappear from the (repeated / mirrored) pattern.

jfkthame commented 2 years ago

It may or may not be obvious to designers to craft a font in such a way that a ColorLine table is actually shared...

True, though whether it is actually shared or not is irrelevant; independently-defined gradients that happen to have similar color-stop placement (and are perhaps subjected to similar variations) would also be expected to behave similarly whether they're linear or radial, but under option (b), they may fail to do so.

A further critique of option (b) that had not occurred to me earlier is that, if a color stop offset were to vary to < 0 and get clamped to 0, then that will affect not just colors at one end of the gradient but also how the pattern mirrors and reflects. So, for example, if two stops toward the tip of a cone were getting compressed because both offsets are varying but one is clamped, then that compression will also occur in each repeat/mirror of the pattern.

Yes; that's essentially the same issue as Dominik mentioned earlier:

If the shader does not receive full/"unclamped" information about all color stops, it can't repeat/reflect correctly...

In view of these issues with (b), I'm finding myself increasingly convinced that (a) is the most reasonable solution available to us. I don't like the fact that it allows the color-stop offsets to perturb the gradient geometry, but this is something that can be spec'd and implemented interoperably; and designers can be warned by a note in the spec about the cases where rendering will deviate from the "ideal" shape.

PeterConstable commented 2 years ago

A different variant of (b) could be to treat stops outside [0,1] as D2D currently does: ignore them. It would still affect the pad/repeat/reflect patterns, so from your example comparing similar radial and linear gradients, it would create a difference — unless the same were also done for linear.

If the Cairo and Skia shaders can't be modified to be more flexible wrt the colour line, it does seem that we'd need to choose between compromising the colour pattern or compromising the geometry.

nedley commented 2 years ago

I am uncomfortable with any solution where the geometry of the gradient is influenced by its color stops or vice versa. COLRv1 implementations should clip or ideally not draw any section with negative radius.

By way of comparison, https://www.w3.org/TR/css-images-3/#radial-color-stops is clear about the behavior of CSS color stops outside the normalized range: such stops will still mathematically influence colors in the normalized range but are not themselves drawn. Since this model is well-defined and already implemented, I suggest hewing as closely to it as possible by allowing stops outside [0, 1], selecting colors only from the normalized range.

My general concern here is that this conversation feels like we are trying to backsolve for some number of existing implementations, rather than figuring out the correct behavior and specifying that.

jfkthame commented 2 years ago

I don't think the CSS gradients spec really addresses the radial-gradient issue here, because it doesn't allow for defining separate starting and ending circles, such that the focus of the gradient falls outside the circles.

nedley commented 2 years ago

I believe COLRv1 radial gradients with r1 > r0 can be expressed as a CSS radial gradient by extrapolating to the starting point, at least geometrically. But my point (of argumentation) is that, since I am not aware of any implementation that exactly maps to what COLRv1 allows, we should avoid a Procrustean resolution as much as we can.

drott commented 2 years ago

@nedley thanks for commenting and looking into this issue. I agree the geometry impact is suboptimal, however, we've from various angles hit the barrier of then having to change fundamentally and maintain modified hardware accelerated shader programs if we don't have a mapping to the existing ones. We find that these are usually optimised towards covering the usual CSS cases - but in this scenario we're hitting situations that are outside of the behaviour of the equivalent CSS gradients.

From the drawRadialGradient documentation I find that the CoreGraphics API also expects 0 and 1 normalized color stops. How tightly optimized are the CoreGraphics shaders, and how easy or difficult would you find it to change these and the CoreGraphics API to accept non-normalized color stop offsets or handling negative radii? (as clipping or color line truncation are not complete solutions) - would this be a solution you would prefer to finding a reasonable mapping to existing shaders?

In the design of COLRv1 we've usually tried to map to existing primitives to avoid overhead implementing new primitives in the graphics libraries - I tend to think it makes more sense to lean in the direction of following that principle here as well.

nedley commented 2 years ago

From the drawRadialGradient documentation I find that the CoreGraphics API also expects 0 and 1 normalized color stops.

Core Graphics doesn't support repeated gradients at all, so I wouldn't consider it to be prior art in this case.

litherum commented 2 years ago

Imagine someone is visualizing a satellite communicating with Earth, and they want to show the communication waves by animating the color stop locations on a radial gradient cone. This seems like a natural use case for radial gradients and animations, which much of this thread seems to be about.

For most of the animation, they will achieve their effect. But then when the values and points align just right, the communication cone starts widening? This wouldn't be what the author wanted.

I understand that extrapolating + running an absolute value is computationally simple, but I don't think it serves font creators / users well.

drott commented 2 years ago

Imagine someone is visualizing a satellite communicating with Earth [...] For most of the animation, they will achieve their effect. But then when the values and points align just right, the communication cone starts widening? This wouldn't be what the author wanted.

This visualization is entirely possible without any unwanted side effects at all if color stop offset 0 and radius 0 are chosen and the source and destination circle center points are moved. What I am trying to make clear: The geometric effects of the proposed solution here only affects IMO very rare edge case situations in which negative radii or negative color stops are chosen or occur due to variations.

nedley commented 2 years ago

Let me put it differently: if it is a choice between compromising the geometry or the color line, we would rather compromise the color line.

PeterConstable commented 2 years ago

his visualization is entirely possible ...

If the visualization Myles has in mind is what I think it is, then your suggestion does not provide that visualization. Moving the destination circle makes the wave stretch, not propagate; moving both circles would appear like a pulse that is moving but not spreading; increasing the two radii would appear like a spreading wave, but only a short pulse. If you wanted to visualize a continuous emmission that spreads, the circles would need to remain the same and you'd need colour stops that animate from before the tip of the cone then along the cone.

PeterConstable commented 2 years ago

Let me put it differently: if it is a choice between compromising the geometry or the color line, we would rather compromise the color line.

Going back to the start of this thread:

But specifying manually a start/end interpolated color and truncating the color line then means that the shader is not aware of the full color-line and repeat modes stop working correctly after the first iteration of the color line.

There appear to be two conflicting perspectives:

There is also a third perspective, which is that neither colour nor geometry should need to be compromised, and I showed above Nick's current DWriteCore implementation which doesn't require any compromise to the colour or geometry. But that is in conflict with one other perspective: that this should be implementable using existing Cairo cairo_pattern_create_radial() and Skia MakeTwoPointConical() APIs.

jfkthame commented 2 years ago

If you wanted to visualize a continuous emmission that spreads, the circles would need to remain the same and you'd need colour stops that animate from before the tip of the cone then along the cone.

I think I understand the visualization you're describing. Can you explain how to render such a visualization using a repeating (or reflecting) gradient via the Cairo, Skia, D2D, SVG, CSS, or HTML Canvas APIs?

PeterConstable commented 2 years ago

Can you explain how to render such a visualization using a repeating (or reflecting) gradient via the Cairo, Skia, D2D, SVG, CSS, or HTML Canvas APIs?

I looked at the reference to CSS Images Module that Ned gave, which says

A color-stop can be placed at a location before 0%; though the negative region of the gradient line is never directly consulted for rendering, color stops placed there can affect the color of non-negative locations on the gradient line through interpolation or repetition...

Using that, I came up with an example using CSS:

https://codepen.io/petercon/pen/zYjNeay

CSS gradients aren't animatable, so I had to provide several keyframes for discrete steps. But the critical thing in this example is the colour line behaviour: colours at offsets < 0 affect interpolation (at 0 and above 0) and repetition. That allows the transition of colours at 0 even though nothing below 0 is being painted.

PeterConstable commented 2 years ago

I've moved my HTML experimentation page to codepen and modified it to all both circle radii to be manipulated.

By making one circle big enough to contain the other, we can explore behaviour for that sub-set of radial gradients which is the extent of what is currently supported in CSS, SVG and many authoring apps such as Figma, Illustrator, etc.

And in this sub-set of radial gradients, if a stop offset gets projected beyond the focal point projected by the two circles (analogous to the tip of the cone), then that point starts getting moved.

image


Addendum: To clarify, I wasn't saying this is a CSS behaviour. I was saying that the proposed revision of the COLRv1 algorithm would have this effect.

litherum commented 2 years ago

Here's the effect I was describing (imagine the "S⃣" is a satellite and the "E⃣" is the earth):

https://user-images.githubusercontent.com/918903/190879408-c4e561dc-872e-47db-b327-81448dd741ca.mov

This seems like a natural use case for a graphics format that allows for color stops outside the 0-1 range. There would be a big color line with alternating color stops, which lie both inside and outside the [0-1] region. The effect would be achieved by animating the position of those color stops linearly over time. In this kind of visualization, I don't think you'd want the cone's shape to change, regardless of the position of the color stops.

Here's the code to generate this effect using Core Graphics: Satellite.zip (Core Graphics doesn't support color stops outside the [0-1] boundary, so the sample project has to polyfill that behavior. You can see more if you look at the code in the above project.)

drott commented 2 years ago

Thanks Myles, for taking the time to build this example and explaining your concerns in more detail. (I spent time with @bungeman today discussing the possible solutions further - thanks to Ben for feedback and the productive discussion. The outcome, Ben and I are aligned on the abs(radius) approach as the proposed resolution.)

Myles, your example can be built in a simpler way in COLRv1 without requiring color stops to protrude outside the [0,1] range. One approach is to define a color line with a very narrow defined color line interval that can be placed anywhere in the middle between [0,1], let's say at 0.5 to 0.55 and be only 0.05 color stops wide, reflect extend mode. As you can see, no cone shapes are affected when the animation is running, and there's no use of any extra clipping in the glyph.

https://roettsch.es/satellite.html is a running COLRv1 form of your example (video below), running in Firefox Nightly, and Chrome Canary when variable COLRv1 is on (chrome://flags/#variable-colrv1). The code for the test glyph based on fonttools is here: https://github.com/drott/color-fonts/commit/1d72f3678235630706f18671cbb6983f397e3cd6 This link shows the coordinates and color stop positions used. The page animates an 'SANI' ("satellite animation") axis of the font.

https://user-images.githubusercontent.com/1021784/191381231-62c4ea1d-facc-4252-b53e-6df0471e5696.mp4

From my perspective, this highlights:

PeterConstable commented 2 years ago

I believe there is a solution that

And I'm surprised we haven't considered this up to now. It does not involve using abs(radius) anywhere; it does not require any clipping or compositing of multiple textures. And it does not limit designers in how they specify gradients.

It involves normalizing the colour line, but in a less simplistic way.

To explain, a few points:

Now, suppose R0 < R1, and suppose the beginning stop offset is < 0 and beyond the tip of the cone. Dominik mentioned above that we could interpolate a colour stop aligning to the tip of the cone and clamp the colour line to that, but that this entails passing an incomplete colour line to the shader, so compromising the extend-mode pattern. Note, however, that this is not a problem for pad extend mode; it is only a problem for repeat and mirror extend modes.

So, suppose that pad extend mode is handled in that way: interpolate a stop at the tip of the cone and truncate the colour line. Let's consider the repeat and mirror modes more carefully.

Something common to both repeat and mirror modes is that there is a repeating pattern. (Note that, for any gradient definition with mirror extend mode there an equivalent gradient definition using repeat mode can be derived by appending stops in reverse order in either direction.) The repeating pattern has a wavelength, λ. (For repeat mode, λ = offsetlast - offsetfirst; for mirror mode, it is double that.)

For any gradient definition, equivalent gradients could be defined using any slice of the colour line of length λ.

Therefore, for repeat/mirror modes, this can be solved by

jfkthame commented 2 years ago

That seems to make perfect sense, at least on initial reading. I'll try to look into implementing it soon, and see if any unexpected complications arise, but it sounds most promising!

jfkthame commented 2 years ago

I've been experimenting with this in the Gecko implementation, and it seems to work well. So if @drott is in agreement (I know he's also done some testing), I'm happy to go forward with this approach.

One thing we still need to explicitly agree on (and document) is how to handle variations applied to the radii, if this results in negative values. I think there are several options:

(1) Clamp the radii to be non-negative at the time of applying variations (before any adjustments for color-line normalization). This is easy to implement, and means that the rendering simply stops varying when the value hits zero.

(2) Define negative radii (resulting from variations) to be invalid, so that a radial gradient where one or both of the radii becomes negative will paint nothing. This is also simple to implement, and arguably most analogous to the behavior of HTML Canvas gradient definitions, though I don't see that it actually benefits authors in any particular way.

(3) Allow negative radii to define a cone, just like positive radii do, but only the cone on the positive-radius side of the tip is to be rendered. This means that in the case of equal negative radii, nothing would be rendered, as the cone degenerates to a negative-radius "imaginary" cylinder and never passes through a tip to become positive-radius, but otherwise this is a simple extension of what we're doing for circles with positive radii. I think this would also be reasonably implementable, though I haven't thoroughly tested it.

Any preferences? I don't have a strong view one way or another; I think any of the above would be OK, we just need to agree.

PeterConstable commented 2 years ago

Thanks, Jonathan, for working on implementation and for enumerating options for radii variations.

I don't care for (2): I think it would be a bit counterintuitive for one of the radii to vary towards zero and then for the entire gradient to suddenly disappear. Or, worse, for the designer to want a cone but then have the gradient disappear because of a small precision error that yields an ever so slightly negative value.

Re (3)

This means that in the case of equal negative radii, nothing would be rendered... The same would happen for (1) when both radii are (clamped to) zero.

I'm pondering what the effective difference would be between (1) and (3). Suppose circle 1 is constant while circle 0 radius varies toward and past zero. IIUC, as the radius goes negative,

(Re the second point, that's also happening while the radius is decreasing but still >0.)

Is that your understanding?

jfkthame commented 2 years ago

Suppose circle 1 is constant while circle 0 radius varies toward and past zero. IIUC, as the radius goes negative,

  • the tip of the visible cone would start moving toward circle 1

  • the geometric alignment of stop offset 0 would be unchanged, but the tip would be moving up the colour line, so less of the colour line is painted.

Yes, that sounds like what I'm picturing.

As I visualize things in this interpretation, a radial gradient where the two defining circles are disjoint can be considered as always describing a double-cone shape (except in the degenerate case where the radii are equal), extending from the "tip" in opposite directions. One direction corresponds to positive radii, and the other to negative. Only the positive-radius cone is actually rendered; the other can be thought of as "behind the paper" or whatever.

If both circles have positive radii, we have the simple case where the cone extends to a tip that is beyond the smaller circle, and grows infinitely beyond the larger one. If one of the radii is negative, the tip falls in between the circles, and the visible cone appears only on the positive-radius side of it. And if both are negative, the cone that they directly describe is entirely in the invisible area, but beyond its tip they "project" a visible cone.

PeterConstable commented 2 years ago

Once (3) is understood, I guess both provide reasonable behaviour with the geometry and colour line working independently. (1) would be easier to describe in the spec, though, and hence less prone to misunderstanding.

jfkthame commented 2 years ago

Agreed, (1) is the simplest to spec and understand, if we're OK with saying that the effect of variations on the radii is restricted in that way. (Which seems logical enough: it's hard to visualize a circle with negative radius! And the basic PaintRadialGradient format has it as an unsigned value, implicitly recognizing that only non-negative values make sense.)

@drott @litherum & others, should we agree to explicitly spec (1), or are there reasons to pursue the (somewhat more complex, but I believe also quite logical) option (3)?

drott commented 2 years ago

I haven't finished the implementation yet, but I believe the extrapolation towards two positive radii with one iteration of the color line in between them can be described soundly mathematically and in the implementation. Hence, I don't think we need any extra clamping rules as in (1), and I would be in favour of (3) (with the reservation that I haven't done this in the code yet). This way we have less discontinuities or hard cut-offs.

There might be some implementation specific practical limits if the projected circles are very far outside a reasonable glyph area, but I don't think that requires any extra spec text if the circles would become practically invisible anyway.

jfkthame commented 2 years ago

I have a patch that aims to implement option (3), and it seems (in initial ad-hoc testing, at least) to be working well. Dealing with the negative radii that may arise from variations is essentially the same as dealing with them in the denormalized-color-line case: for extend-pad, we can just modify the color line as needed to correspond to the part of the cone that is visible, while for the repeating modes, we can move it by units of the "wavelength" in order to put it where it'll work for the shader.

@PeterConstable Given Dominik's preference for (3), are you happy to agree on this and we'll move forward with implementations? I think the main thing needed in the spec is some text describing what it means for the radial-gradient circle radii to become negative due to variations,(*) and perhaps a (non-normative) implementation note explaining the suggested approach to implementing this, as well as color lines that are defined outside the visible cone, for graphics backends/shaders that cannot directly handle the negative values.

(*) Maybe the desired result automatically falls out of the algorithm already given at https://learn.microsoft.com/en-gb/typography/opentype/spec/colr#radial-gradients? I haven't attempted to work through what happens if negative radii are used there. But in any case, I think examples of what happens if one or both radii are negative should be included in the description of the results, as it's not immediately obvious what to expect.

niklasb-ms commented 2 years ago

I agree with modifying the spec to say radii cannot be negative, and any negative radii resulting from variations are clamped to zero.

The existing algorithm described in the spec does already yield the desired result. I based my implementation on that algorithm and got the correct result (except that I overlooked "where r(ω) > 0"). I can see some value in adding a non-normative implementation note, but I suspect that to give enough context it might end up having to be rather long.

jfkthame commented 2 years ago

I agree with modifying the spec to say radii cannot be negative, and any negative radii resulting from variations are clamped to zero.

That's not what @drott favoured in his comment yesterday, and I'm inclined to agree with him that no clamping should be done. Nothing is rendered for cones where the radii are negative, but such a cone (unless the radii are equal) has a "projection" into positive-radius space, and it seems clear to us how that should render.

What would the existing algorithm in the spec actually render if given negative radii? I don't have a direct implementation of it to test, but it doesn't appear to me that the answer would be "nothing", as r(ω) can perfectly well be greater than 0 for some values of ω even if one or both of r0 and r1 are negative.

niklasb-ms commented 2 years ago

Ah, I overlapped @drott's comment from yesterday. I guess I'm OK either way. I'm just glad there's a solution that allows to avoid modifying the geometry by taking the absolute value of the radius.

It might be helpful for the spec to explicitly note that the radii can be negative as a result of variations, even though the fields in the paint structures are unsigned. I think the existing algorithm should just work in that case, but I haven't tried it yet.

PeterConstable commented 2 years ago

For my part, I could live with either option (1) or (3).

It would be nice to hear from folk at Adobe and Apple, but I wouldn't want to keep this open much longer: we have three companies wanting to proceed with their implementations, OT 1.9.1 needs to progress, and I need to get a contribution doc to SC29/WG3 before the up-coming meeting. I'll poke folk at Adobe and Apple and wait another day before I proceed with a COLR revision in OT 1.9.1 alpha.

niklasb-ms commented 2 years ago

I just tested my implementation with a negative radius (using test_glyphs-glyf_colr_1_variable.ttf and changing the GRR0 or GRR1 axis) and there is a very dramatic discontinuity going from 0 to a small negative number. This could be a bug in my implementation, or it could just be that the algorithm described in the spec doesn't work well for negative radii.

For this reason, I would be in favor of clamping negative radii to 0. I think this is reasonable, easy to explain, and easy to understand.

I think this is quite different in kind from the earlier proposal to take the absolute value of the radius because in this case we're talking about the actual radius specified by the font (after variations are applied). From the data structure, it's clear the radii are intended to be non-negative, so ensuring they're still non-negative after variations is reasonable.

In the earlier absolute value proposal, the radii in question were not the original values from the font. They were a result of the implementation moving the circle for implementation-specific reasons that are not easy to explain or understand.

jfkthame commented 2 years ago

there is a very dramatic discontinuity going from 0 to a small negative number

That's not my observation with the implementation I'm working on (which does not directly implement the algorithm from the spec, but maps the gradient specified by the font to existing shader APIs as we've been discussing here); the behavior as a radius passes through zero is nicely continuous.