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 missing elements are copied 
from the target list. If the start list has more elements than the target list, 
the excess elements are discarded. |
| (see prose) | Some component types are interpolated in specific ways not 
covered here. Refer to their respective documentation for more information. |

Every component of an interpolatable class specifies its interpolation type 
with the new `@interpolationType` javadoc tag. For example, this is the 
specification of the `CornerRadii.topLeftHorizontalRadius` component:

    /**
     * The length of the horizontal radii of the top-left corner.
     *
     * @return the length of the horizontal radii of the top-left corner
     * @interpolationType <a 
href="../../animation/Interpolatable.html#linear">linear</a> if both values are
     *                    absolute or both values are {@link 
#isTopLeftHorizontalRadiusAsPercentage() percentages},
     *                    <a 
href="../../animation/Interpolatable.html#discrete">discrete</a> otherwise
     */
    public double getTopLeftHorizontalRadius()


In the generated documentation, this javadoc tag shows up as a new entry in the 
specification list:

<img 
src="https://github.com/openjdk/jfx/assets/43553916/c4f66153-632e-4e44-a160-fccbdab4f87a";
 width="500">

## Independent transitions of sub-properties

CSS allows developers to specify independent transitions for sub-properties. 
Consider the following example:

.button {
    -fx-border-color: red;
    -fx-border-width: 5;

    transition: -fx-border-color 1s linear,
                -fx-border-width 4s ease;
}

.button:hover {
    -fx-border-color: green;
    -fx-border-width: 10;
}

We have two independent transitions (each with a different duration and easing 
function) that target two sub-properties of the same `Border`. We can't use 
normal interpolation between the before-change style and the after-change style 
of the border, as the `Interpolatable` interface doesn't allow us to specify 
separate transitions per sub-property.

Instead, we introduce the marker interface `javafx.css.ComponentTransitionable` 
for types that expose sub-properties via CSS. `Border` and `Background` both 
implement this marker interface.

In addition to that, we add a new optional operation to `StyleConverter` (which 
is implemented by `BorderConverter` and `BackgroundConverter`):

    /**
     * Converts an object back to a map of its constituent values 
(deconstruction).
     * The returned map can be passed into {@link #convert(Map)} to reconstruct 
the object.
     *
     * @param value the object
     * @throws UnsupportedOperationException if this converter does not support 
deconstruction
     * @return a {@code Map} of the constituent values
     * @apiNote This is an optional operation. The default implementation of 
this
     *          method throws {@code UnsupportedOperationException}.
     * @implSpec The following invariant must be satisfied: {@code 
convert(convertBack(value)).equals(value)}
     * @since 24
     */
    public Map<CssMetaData<? extends Styleable, ?>, Object> convertBack(T value)


This allows us to use style converters to decompose solid objects into their 
CSS-addressable component parts, animate each of the components, and then 
reconstruct a new solid object that incorporates the effects of several 
independent transitions.

More specifically, if the CSS subsystem applies a new value via 
`StyleableProperty.applyStyle(newValue)`:
1. If `newValue` implements `ComponentTransitionable`:
    a. Deconstruct `oldValue` and `newValue` into their components.
    b. For each component, determine if a transition was specified; if so, 
start a transition from `oldValue.componentN` to `newValue.componentN` with 
rules as described in "Interpolation of composite objects".
    c. For each frame, collect the effects of all component transitions, and 
reconstruct the current value as a solid object.
2. Otherwise, if `newValue` implements `Interpolatable`:
    Start a regular transition using `Interpolatable.interpolate()`.

## Limitations

Implementations usually fall back to discrete interpolation when the start 
value is an absolute value, and the end value is a percentage (see the example 
of `CornerRadii.topLeftHorizontalRadius` above). However, we can often solve 
these scenarios by first canonicalizing the values before interpolation (i.e. 
converting percentages to absolute values). This will be a future enhancement.

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

Commit messages:
 - fix line separators
 - documentation
 - add tests
 - Merge branch 'master' into feature/interpolatable
 - fix line separators
 - ComponentTransitionable implementation
 - fix line separators
 - add documentation to Point2D/3D
 - change documentation
 - add specification
 - ... and 9 more: https://git.openjdk.org/jfx/compare/72701e6c...94d6a90b

Changes: https://git.openjdk.org/jfx/pull/1522/files
  Webrev: https://webrevs.openjdk.org/?repo=jfx&pr=1522&range=00
  Issue: https://bugs.openjdk.org/browse/JDK-8332895
  Stats: 6064 lines in 77 files changed: 5325 ins; 175 del; 564 mod
  Patch: https://git.openjdk.org/jfx/pull/1522.diff
  Fetch: git fetch https://git.openjdk.org/jfx.git pull/1522/head:pull/1522

PR: https://git.openjdk.org/jfx/pull/1522

Reply via email to