RFR: 8332895: Support interpolation for backgrounds and borders [v2]

Andy Goryachev angorya at openjdk.org
Thu Aug 1 23:57:46 UTC 2024


On Wed, 31 Jul 2024 19:34:12 GMT, Michael Strauß <mstrauss at openjdk.org> wrote:

>> This PR completes the CSS Transitions story (see #870) by adding interpolation support for backgrounds and borders, making them targetable by transitions.
>> 
>> `Background` and `Border` objects are deeply immutable, but not interpolatable. Consider the following `Background`, which describes the background of a `Region`:
>> 
>> 
>> Background {
>>     fills = [
>>         BackgroundFill {
>>             fill = Color.RED
>>         }
>>     ]
>> }
>> 
>> 
>> Since backgrounds are deeply immutable, changing the region's background to another color requires the construction of a new `Background`, containing a new `BackgroundFill`, containing the new `Color`.
>> 
>> Animating the background color using a CSS transition therefore requires the entire Background object graph to be interpolatable in order to generate intermediate backgrounds.
>> 
>> More specifically, the following types will now implement `Interpolatable`.
>> 
>> - `Insets`
>> - `Background`
>> - `BackgroundFill`
>> - `BackgroundImage`
>> - `BackgroundPosition`
>> - `BackgroundSize`
>> - `Border`
>> - `BorderImage`
>> - `BorderStroke`
>> - `BorderWidths`
>> - `CornerRadii`
>> - `Stop`
>> - `Paint` and all of its subclasses
>> - `Margins` (internal type)
>> - `BorderImageSlices` (internal type)
>> 
>> ## Interpolation of composite objects
>> 
>> As of now, only `Color`, `Point2D`, and `Point3D` are interpolatable. Each of these classes is an aggregate of `double` values, which are combined using linear interpolation. However, many of the new interpolatable classes comprise of not only `double` values, but a whole range of other types. This requires us to more precisely define what we mean by "interpolation".
>> 
>> Mirroring the CSS specification, the `Interpolatable` interface defines several types of component interpolation:
>> 
>> | Interpolation type | Description |
>> |---|---|
>> | default | Component types that implement `Interpolatable` are interpolated by calling the `interpolate(Object, double)}` method. |
>> | linear | Two components are combined by linear interpolation such that `t = 0` produces the start value, and `t = 1` produces the end value. This interpolation type is usually applicable for numeric components. |
>> | discrete | If two components cannot be meaningfully combined, the intermediate component value is equal to the start value for `t < 0.5` and equal to the end value for `t >= 0.5`. |
>> | pairwise | Two lists are combined by pairwise interpolation. If the start list has fewer elements than the target list, the...
>
> Michael Strauß has updated the pull request incrementally with one additional commit since the last revision:
> 
>   fixed a bug

Not a complete review, but the first batch of comments (I stopped at Stop, will continue).  I think it's better to send smaller batches than one giant one (and I am afraid losing the comments due to some GH glitch).

modules/javafx.graphics/src/main/java/javafx/animation/Interpolatable.java line 37:

> 35:  *     <caption><b>Interpolation types</b></caption>
> 36:  *     <tbody>
> 37:  *         <tr><td><a id="default" style="white-space: nowrap">default</a></td>

very minor: is it possible to top-align the first column content instead of center?

something like
`<td style="vertical-align: top;">`

modules/javafx.graphics/src/main/java/javafx/animation/Interpolatable.java line 57:

> 55:  *             </td>
> 56:  *         </tr>
> 57:  *         <tr><td style="white-space: nowrap">(see prose)</td>

"see prose" - could you be more specific?  maybe give an example?

modules/javafx.graphics/src/main/java/javafx/css/StyleableDoubleProperty.java line 157:

> 155:         @Override
> 156:         public boolean updateReversingAdjustedStartValue(TransitionMediator existingMediator) {
> 157:             var mediator = (TransitionMediatorImpl)existingMediator;

minor: should we use an instanceof pattern here, or do we want to throw ClassCastException?
(here and elsewhere)

example:

if(existingMediator instanceof TransitionMediatorImpl mediator) {
...
}

modules/javafx.graphics/src/main/java/javafx/css/StyleableLongProperty.java line 136:

> 134:         @Override
> 135:         public void onUpdate(double progress) {
> 136:             set(progress < 1 ? startValue + (long)((endValue - startValue) * progress) : endValue);

minor: should it use (long)Math.round() ?

modules/javafx.graphics/src/main/java/javafx/css/StyleableObjectProperty.java line 124:

> 122:             // value of the existing transition. This scenario can sometimes happen when a CSS value
> 123:             // is redundantly applied, which would cause unexpected animations if we allowed the new
> 124:             // transition to interrupt the existing transition.

TODO
is there a unit test for this scenario?

modules/javafx.graphics/src/main/java/javafx/css/StyleableObjectProperty.java line 210:

> 208:             // the running transition by adding its mediator to the newly created transition controller
> 209:             // that manages the hover->base transition. In this way, the new transition controller will
> 210:             // continue to aggregate the effects of the pre-existing transition.

TODO
add a unit test?

modules/javafx.graphics/src/main/java/javafx/css/StyleableObjectProperty.java line 273:

> 271: 
> 272:     private StyleOrigin origin = null;
> 273:     private TransitionController<T> controller = null;

minor: `= null` is unnecessary

modules/javafx.graphics/src/main/java/javafx/css/StyleableObjectProperty.java line 312:

> 310:             // refers to the reversing controller, and not to this controller. We need to be careful to only
> 311:             // clear references to this controller.
> 312:             if (controller == this) {

is it guaranteed that the reference will be cleared eventually?
TODO
is there a unit test?

modules/javafx.graphics/src/main/java/javafx/css/StyleableObjectProperty.java line 465:

> 463:                 if (mediators.get(i) == mediator) {
> 464:                     mediators.remove(i);
> 465:                     break;

how do we ensure there are no duplicates in the `mediators` list?

modules/javafx.graphics/src/main/java/javafx/css/StyleableObjectProperty.java line 538:

> 536:                 } else if (startValue instanceof Interpolatable && endValue instanceof Interpolatable) {
> 537:                     value = ((Interpolatable<U>)startValue).interpolate(endValue, progress);
> 538:                 } else {

minor: does this if-else block ordered by the expected frequency?

modules/javafx.graphics/src/main/java/javafx/scene/Node.java line 9010:

> 9008:          *
> 9009:          * @param property the {@code StyleableProperty}
> 9010:          * @param result the map into which results are stored, should be {@code null}

should be or could be?
see L9032

modules/javafx.graphics/src/main/java/javafx/scene/Node.java line 9184:

> 9182:          *
> 9183:          * @param metadata the CSS metadata of the property
> 9184:          * @param result the map into which results are stored, should be {@code null}

same comment, `could be null`?

modules/javafx.graphics/src/main/java/javafx/scene/layout/Background.java line 301:

> 299:         boolean opaqueFill = false;
> 300: 
> 301:         // We need to iterate over all of the supplied elements in the fills list.

TODO
check if there is a unit test for this

modules/javafx.graphics/src/main/java/javafx/scene/layout/BackgroundFill.java line 157:

> 155:         return this.fill == fill
> 156:             && this.radii == radii
> 157:             && this.insets == insets;

are you sure these don't need equals()?

modules/javafx.graphics/src/main/java/javafx/scene/layout/BackgroundImage.java line 212:

> 210:             && this.repeatY == repeatY
> 211:             && this.position == position
> 212:             && this.size == size;

same comment on equals().

'size' comes from interpolate() on L178, so there is no guarantee as of its identity

modules/javafx.graphics/src/main/java/javafx/scene/layout/BackgroundPosition.java line 266:

> 264:                            Side verticalSide, double verticalPosition, boolean verticalAsPercentage) {
> 265:         return this.horizontalSide == horizontalSide
> 266:             && this.horizontalPosition == horizontalPosition

so here `==` seems to be ok since we are comparing enums and primitive values, but...
should we account for floating point errors that might appear during interpolation?
i.e.

isClose(this.horizontalPosition, horizontalPosition) &&


where

private static boolean isClose(double a, double b) {
 return Math.abs(a - b) < EPSILON; // 0.00001 or something like that should be ok for screen coordinates
}

modules/javafx.graphics/src/main/java/javafx/scene/layout/BackgroundSize.java line 258:

> 256:         return this.width == width
> 257:             && this.height == height
> 258:             && this.widthAsPercentage == widthAsPercentage

same comment - use isClose() for doubles?

modules/javafx.graphics/src/main/java/javafx/scene/layout/Border.java line 331:

> 329: 
> 330:         for (int i = 0, max = strokes.size(); i < max; i++) {
> 331:             final BorderStroke stroke = strokes.get(i);

just curious: do we really need `final` keyword for local variables after java8?

modules/javafx.graphics/src/main/java/javafx/scene/layout/BorderConverter.java line 270:

> 268:             case ROUND -> BackgroundRepeat.ROUND;
> 269:             case SPACE -> BackgroundRepeat.SPACE;
> 270:             case STRETCH -> BackgroundRepeat.NO_REPEAT;

what do you think of adding default: case?
this code will not compile if BorderRepeat ever acquires a new value.

modules/javafx.graphics/src/main/java/javafx/scene/layout/BorderConverter.java line 279:

> 277:             case ROUND -> BorderRepeat.ROUND;
> 278:             case SPACE -> BorderRepeat.SPACE;
> 279:             case NO_REPEAT -> BorderRepeat.STRETCH;

same here // "non-comprehensive switch expression over evolved enum problem"

modules/javafx.graphics/src/main/java/javafx/scene/paint/Color.java line 1956:

> 1954: 
> 1955:         // If both instances are equal, return this instance to prevent the creation of numerous small objects.
> 1956:         if (t <= 0.0 || equals(endValue)) return this;

This question might be outside of the current PR, but I think the naive color interpolation algorithm is a bad choice here (L1960).

See for example
https://www.alanzucconi.com/2016/01/06/colour-interpolation/

The question that is relevant to this PR is whether we should enable the user to specify the interpolation algorithm.

-------------

PR Review: https://git.openjdk.org/jfx/pull/1522#pullrequestreview-2213806821
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700900660
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700902994
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700911902
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700915376
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700918183
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700925072
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700933990
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700936434
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700942231
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700956059
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700960388
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700962134
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700967627
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700988085
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700990366
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700995142
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700997569
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1700999656
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1701010560
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1701011685
PR Review Comment: https://git.openjdk.org/jfx/pull/1522#discussion_r1701018016


More information about the openjfx-dev mailing list