Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8311895: CSS Transitions #870

Closed
wants to merge 63 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
7195ae9
Implementation of CSS transitions
mstr2 Aug 15, 2022
662345f
allow transition with zero duration
mstr2 Aug 16, 2022
7b577f9
Running transition timers are stopped when their target property is c…
mstr2 Aug 16, 2022
32e427b
Transitions are stopped when the Node is removed from the scene
mstr2 Aug 16, 2022
cf1976f
Added TransitionEvent
mstr2 Aug 21, 2022
3ce272e
typo
mstr2 Aug 21, 2022
8d09466
Added CSS documentation
mstr2 Aug 24, 2022
0046c6c
Fixed default values for transition properties
mstr2 Aug 24, 2022
13d0fd9
Factor out StyleableTimer
mstr2 Aug 24, 2022
2cfe17d
Added documentation for TransitionDefinition
mstr2 Aug 24, 2022
e0b1ad4
Events are fired when duration is zero
mstr2 Aug 25, 2022
7ceb41d
Refactored duration parsing
mstr2 Aug 25, 2022
abc8475
Ensure that property names are case-sensitive
mstr2 Aug 26, 2022
083fb00
Fix case sensitivity of 'all' identifier
mstr2 Aug 26, 2022
d983308
Refactored TransitionTimer
mstr2 Aug 27, 2022
e34a549
Removed public Node API
mstr2 Jul 11, 2023
2556c7c
Revert changes
mstr2 Jul 11, 2023
559c9b5
Make step interpolators public API
mstr2 Jul 12, 2023
1ddeef4
Added FX compat interpolators
mstr2 Jul 12, 2023
175ce1e
Fix interpolator cache
mstr2 Jul 13, 2023
5b52a80
Remove AbstractPropertyTimer
mstr2 Jul 13, 2023
ce29bfe
Refactor TransitionTimer.tryStop
mstr2 Jul 13, 2023
b68554f
Add tests
mstr2 Jul 14, 2023
3a308bf
Added fast reversing of interrupted transitions
mstr2 Jul 15, 2023
59d5e1e
Remove unused code
mstr2 Jul 15, 2023
2527b83
Refactoring
mstr2 Jul 15, 2023
99078b5
Added documentation
mstr2 Jul 15, 2023
8ff3674
Refactoring
mstr2 Jul 16, 2023
80561f6
Start transitions only for showing nodes
mstr2 Jul 17, 2023
5e179cb
Intern parsed property names
mstr2 Jul 17, 2023
1fc2c03
Refactoring
mstr2 Jul 25, 2023
2607a1e
Redundant transitions are discarded
mstr2 Jul 26, 2023
d68cfe7
Added manual test application
mstr2 Jul 26, 2023
e1e0db6
Removed trailing whitespace
mstr2 Jul 26, 2023
9cdeb49
Removed test
mstr2 Jul 26, 2023
8e46c89
Make TransitionEvent final
mstr2 Jul 31, 2023
94fa9d2
Review changes
mstr2 Jul 31, 2023
66ee849
Make interpolator fields final
mstr2 Jul 31, 2023
deb65ea
Review changes
mstr2 Jul 31, 2023
da61e64
Review changes
mstr2 Aug 1, 2023
0b77908
Added documentation
mstr2 Aug 9, 2023
035fe5c
Merge branch 'master' into feature/css-transitions
mstr2 Oct 28, 2023
482b1ec
Test whether two Interpolatable instances are compatible
mstr2 Oct 31, 2023
0fc3861
Add TransitionMediator
mstr2 Mar 18, 2024
df39f3f
Merge branch 'master' into feature/css-transitions
mstr2 Mar 18, 2024
b76568c
Merge branch 'master' into feature/css-transitions
mstr2 Mar 18, 2024
22aa4d6
Changes per review
mstr2 Mar 18, 2024
1595a19
Discard redundant transitions in StyleableProperty impls
mstr2 Mar 31, 2024
7edb374
Merge branch 'master' into feature/css-transitions
mstr2 Mar 31, 2024
f7ab6db
Change javadoc comment
mstr2 Mar 31, 2024
f996f8f
Fix javadoc error
mstr2 Mar 31, 2024
137a00c
update 'since' tags
mstr2 Mar 31, 2024
6614abb
Merge branch 'master' into feature/css-transitions
mstr2 May 2, 2024
eb60862
fixed documentation
mstr2 May 24, 2024
1a7b720
use existing property in test
mstr2 May 24, 2024
8c6b9a4
extract magic string to named constant
mstr2 May 24, 2024
a43dee3
Merge branch 'refs/heads/master' into feature/css-transitions
mstr2 May 24, 2024
d3184e6
address review comments
mstr2 May 25, 2024
4cb6c87
added documentation
mstr2 May 25, 2024
f64ad70
documentation change
mstr2 May 26, 2024
8a938dc
document css parser limitation
mstr2 May 28, 2024
10c06a9
Merge branch 'refs/heads/master' into feature/css-transitions
mstr2 May 31, 2024
29f43ed
link to easing function images in scene
mstr2 Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Review changes
mstr2 committed Jul 31, 2023
commit 94fa9d2a85836927ae8c4b436d6a99586027aa83
Original file line number Diff line number Diff line change
@@ -1716,20 +1716,20 @@ <h3><a id="typeeasingfunction">&lt;easing-function&gt;</a></h3>
<tr><td><br/></td></tr>
<tr>
<td style="width: 120px; vertical-align: top"><span class="grammar">jump-start</span></td>
<td>the interval starts with a rise point when the input progress value is 0</span></td>
<td>the interval starts with a rise point when the input progress value is 0</td>
</tr>
<tr>
<td style="width: 120px; vertical-align: top"><span class="grammar">jump-end</span></td>
<td>the interval ends with a rise point when the input progress value is 1</span></td>
<td>the interval ends with a rise point when the input progress value is 1</td>
</tr>
<tr>
<td style="width: 120px; vertical-align: top"><span class="grammar">jump-none</span></td>
<td>all rise points are within the open interval (0..1)</span></td>
<td>all rise points are within the open interval (0..1)</td>
</tr>
<tr>
<td style="width: 120px; vertical-align: top"><span class="grammar">jump-both</span></td>
<td>the interval starts with a rise point when the input progress value is 0,
and ends with a rise point when the input progress value is 1</span></td>
and ends with a rise point when the input progress value is 1</td>
</tr>
<tr>
<td style="width: 120px; vertical-align: top"><span class="grammar">start</span></td>
Original file line number Diff line number Diff line change
@@ -86,7 +86,7 @@ public StyleableProperty<String[]> getStyleableProperty(S styleable) {
return null;
}
},
new CssMetaData<S, Duration[]>( "transition-duration",
new CssMetaData<S, Duration[]>("transition-duration",
DurationConverter.SequenceConverter.getInstance(), DURATION_ZERO, false) {
@Override
public boolean isSettable(S styleable) {
@@ -98,7 +98,7 @@ public StyleableProperty<Duration[]> getStyleableProperty(S styleable) {
return null;
}
},
new CssMetaData<S, Duration[]>( "transition-delay",
new CssMetaData<S, Duration[]>("transition-delay",
DurationConverter.SequenceConverter.getInstance(), DURATION_ZERO, false) {
@Override
public boolean isSettable(S styleable) {
@@ -110,7 +110,7 @@ public StyleableProperty<Duration[]> getStyleableProperty(S styleable) {
return null;
}
},
new CssMetaData<S, Interpolator[]>( "transition-timing-function",
new CssMetaData<S, Interpolator[]>("transition-timing-function",
InterpolatorConverter.SequenceConverter.getInstance(), INTERPOLATOR_EASE, false) {
@Override
public boolean isSettable(S styleable) {
Original file line number Diff line number Diff line change
@@ -41,14 +41,17 @@
/**
* {@code TransitionTimer} is the base class for timers that compute intermediate
* values for implicit transitions of a {@link StyleableProperty}.
*
* @param <T> the value type
* @param <P> the property type
*/
public abstract class TransitionTimer<T, P extends Property<T> & StyleableProperty<T>> extends AnimationTimer {

private final P property;
private Interpolator interpolator;
private long startTime, endTime, delay, duration;
private double reversingShorteningFactor;
private long currentTime;
private long startTime, endTime, delay, duration; // in nanoseconds
private long currentTime; // in nanoseconds
private boolean updating;
private boolean started;

@@ -61,14 +64,18 @@ protected TransitionTimer(P property) {
* If the combined duration of the transition is zero or if the targeted node is not
* showing, no transition timer is started, no events are dispatched, and this method
* returns {@code null}.
*
* @param timer the {@code TransitionTimer} that will be started
* @param transition the {@code TransitionDefinition} used to initialize the {@code timer}
* @return the {@code timer} instance if the timer was started, {@code null} otherwise
*/
@SuppressWarnings("unchecked")
public static <T, P extends Property<T> & StyleableProperty<T>> TransitionTimer<T, P> run(
TransitionTimer<T, P> timer, TransitionDefinition transition) {
long now = AnimationTimerHelper.getPrimaryTimer(timer).nanos();
timer.interpolator = transition.interpolator();
timer.delay = (long)(transition.delay().toMillis() * 1_000_000);
timer.duration = (long)(transition.duration().toMillis() * 1_000_000);
timer.delay = millisToNanos(transition.delay().toMillis());
timer.duration = millisToNanos(transition.duration().toMillis());
timer.currentTime = now;
timer.startTime = now + timer.delay;
timer.endTime = timer.startTime + timer.duration;
@@ -94,14 +101,14 @@ public static <T, P extends Property<T> & StyleableProperty<T>> TransitionTimer<
// timer, which means that the new timer is a reversing timer that needs to be
// adjusted by the reversing shortening algorithm.
if (combinedDuration > 0) {
adjustReversingTimer(existingTimer, timer, transition, now);
adjustReversingTimer(existingTimer, timer, now);
}

existingTimer.stop(TransitionEvent.CANCEL);
}

if (combinedDuration > 0) {
timer.start();
timer.startImpl();
return timer;
}
}
@@ -151,10 +158,14 @@ public static boolean cancel(TransitionTimer<?, ?> timer, boolean forceStop) {
* now takes the entire duration (4s) to progress just one quarter of the original value space,
* which means that the transition speed is much slower than what would be expected.
*
* @param existingTimer the timer of the running transition
* @param newTimer the timer of the new transition
* @param now the current time, in nanoseconds
* @see <a href="https://www.w3.org/TR/css-transitions-1/#reversing">Faster reversing of interrupted transitions</a>
*/
private static void adjustReversingTimer(TransitionTimer<?, ?> existingTimer, TransitionTimer<?, ?> newTimer,
TransitionDefinition transition, long now) {
private static void adjustReversingTimer(TransitionTimer<?, ?> existingTimer,
TransitionTimer<?, ?> newTimer,
long now) {
double progress = InterpolatorHelper.curve(existingTimer.interpolator, existingTimer.getProgress());

if (progress > 0 && progress < 1) {
@@ -164,36 +175,63 @@ private static void adjustReversingTimer(TransitionTimer<?, ?> existingTimer, Tr
}

if (newTimer.delay < 0) {
double adjustedDelay = transition.delay().toMillis() * newTimer.reversingShorteningFactor;
newTimer.startTime = now + (long)(adjustedDelay * 1_000_000);
double adjustedDelay = nanosToMillis(newTimer.delay) * newTimer.reversingShorteningFactor;
newTimer.startTime = now + millisToNanos(adjustedDelay);
}

double adjustedDuration = transition.duration().toMillis() * newTimer.reversingShorteningFactor;
newTimer.endTime = newTimer.startTime + (long)(adjustedDuration * 1_000_000);
double adjustedDuration = nanosToMillis(newTimer.duration) * newTimer.reversingShorteningFactor;
newTimer.endTime = newTimer.startTime + millisToNanos(adjustedDuration);
}

/**
* Converts the specified duration in nanoseconds to fractional milliseconds.
*
* @param nanos the duration in nanoseconds
* @return the duration in fractional milliseconds
*/
private static double nanosToMillis(long nanos) {
long millis = nanos / 1_000_000L;
double frac = (double)(nanos - (millis * 1_000_000L)) / 1_000_000D;
return (double)millis + frac;
}

/**
* Converts the specified duration in fractional milliseconds to nanoseconds.
*
* @param millis the duration in fractional milliseconds
* @return the duration in nanoseconds
*/
private static long millisToNanos(double millis) {
long wholeMillis = (long)millis;
double frac = millis - (double)wholeMillis;
return wholeMillis * 1_000_000L + (long)(frac * 1_000_000D);
}

/**
* Fires a {@link TransitionEvent} of the specified type.
* The elapsed time is computed according to the CSS Transitions specification.
*
* @param timer the {@code TransitionTimer} that fires the event
* @param eventType the event type
*/
private static <T, P extends Property<T> & StyleableProperty<T>> void fireTransitionEvent(
TransitionTimer<T, P> timer, EventType<TransitionEvent> eventType) {
try {
double elapsedTime;
Duration elapsedTime;

// Elapsed time specification: https://www.w3.org/TR/css-transitions-1/#event-transitionevent
if (eventType == TransitionEvent.RUN || eventType == TransitionEvent.START) {
elapsedTime = (double)Math.min(Math.max(-timer.delay, 0), timer.duration) / 1_000_000.0;
elapsedTime = Duration.millis(nanosToMillis(Math.min(Math.max(-timer.delay, 0), timer.duration)));
} else if (eventType == TransitionEvent.CANCEL) {
elapsedTime = (double)Math.max(0, timer.currentTime - timer.startTime) / 1_000_000.0;
elapsedTime = Duration.millis(nanosToMillis(Math.max(0, timer.currentTime - timer.startTime)));
} else if (eventType == TransitionEvent.END) {
elapsedTime = (double)timer.duration / 1_000_000.0;
elapsedTime = Duration.millis(nanosToMillis(timer.duration));
} else {
throw new IllegalArgumentException("eventType");
}

Node node = (Node)timer.getProperty().getBean();
node.fireEvent(new TransitionEvent(eventType, timer.getProperty(), Duration.millis(elapsedTime)));
node.fireEvent(new TransitionEvent(eventType, timer.getProperty(), elapsedTime));
} catch (Throwable ex) {
Thread currentThread = Thread.currentThread();
currentThread.getUncaughtExceptionHandler().uncaughtException(currentThread, ex);
@@ -202,6 +240,8 @@ private static <T, P extends Property<T> & StyleableProperty<T>> void fireTransi

/**
* Gets the styleable property targeted by this {@code TransitionTimer}.
*
* @return the styleable property
*/
public final P getProperty() {
return property;
@@ -238,13 +278,11 @@ public final void handle(long now) {
}

/**
* Starts this timer, and adds it to the list of running transitions.
* This method is unused, calling it will throw {@link UnsupportedOperationException}.
*/
@Override
public final void start() {
super.start();
NodeHelper.addTransitionTimer((Node)property.getBean(), this);
fireTransitionEvent(this, TransitionEvent.RUN);
throw new UnsupportedOperationException();
}

/**
@@ -259,6 +297,8 @@ public final void stop() {
* Stops the running transition and fires the specified event.
* This happens when the value of a CSS property targeted by a transition is changed by the user,
* when the transition is interrupted by another transition, or when it ends normally.
*
* @param eventType the event type that is fired after the timer is stopped
*/
public final void stop(EventType<TransitionEvent> eventType) {
super.stop();
@@ -276,6 +316,15 @@ public final void complete() {
stop(TransitionEvent.CANCEL);
}

/**
* Starts this timer, and adds it to the list of running transitions.
*/
private void startImpl() {
super.start();
NodeHelper.addTransitionTimer((Node)property.getBean(), this);
fireTransitionEvent(this, TransitionEvent.RUN);
}

/**
* Updates the transition timer by mapping the specified input progress to an output progress
* value using the timer's interpolator, and then calling {@link #onUpdate(P, double)}} with the
@@ -298,6 +347,8 @@ private void update(double progress) {
/**
* Polls whether the timer is currently updating the value of the property.
* After this method is called, the {@link #updating} flag is {@code false}.
*
* @return {@code true} if the timer is currently updating the property, {@code false} otherwise
*/
private boolean pollUpdating() {
boolean updating = this.updating;
@@ -306,7 +357,9 @@ private boolean pollUpdating() {
}

/**
* Gets the progress of this timer along the input progress axis, ranging from 0 to 1.
* Gets the progress of this timer along the input progress axis.
*
* @return the input progress, ranging from 0 to 1
*/
private double getProgress() {
if (currentTime <= startTime) {
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@
* Implementation of a step interpolator as described by
* <a href="https://www.w3.org/TR/css-easing-1/#step-easing-algo">CSS Easing Functions Level 1</a>
*/
public class StepInterpolator extends Interpolator {
public final class StepInterpolator extends Interpolator {

private final int intervals;
private final StepPosition position;
22 changes: 12 additions & 10 deletions modules/javafx.graphics/src/main/java/javafx/css/CssParser.java
Original file line number Diff line number Diff line change
@@ -675,7 +675,7 @@ private Size time(Token token) throws ParseException {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("Expected \'<duration>\'");
}
ParseException re = new ParseException("Expected \'<duration>\'",token, this);
ParseException re = new ParseException("Expected \'<duration>\'", token, this);
reportError(createError(re.toString()));
throw re;
}
@@ -1012,14 +1012,14 @@ private ParsedValueImpl<?, Size> parseTime(final Term root) throws ParseExceptio
if (root.token.getType() != CssLexer.IDENT) {
Size time = time(root.token);
return new ParsedValueImpl<>(time, null);
} else {
String key = root.token.getText();
return switch (key) {
case "initial", "inherit" -> new ParsedValueImpl<>(new Size(0, SizeUnits.S), null);
case "indefinite" -> new ParsedValueImpl<>(new Size(Double.POSITIVE_INFINITY, SizeUnits.S), null);
default -> new ParsedValueImpl<>(key, null, true);
};
}

String key = root.token.getText();
return switch (key) {
case "initial", "inherit" -> new ParsedValueImpl<>(new Size(0, SizeUnits.S), null);
case "indefinite" -> new ParsedValueImpl<>(new Size(Double.POSITIVE_INFINITY, SizeUnits.S), null);
default -> new ParsedValueImpl<>(key, null, true);
};
}

private ParsedValueImpl<ParsedValue<?, Size>, Duration> parseDuration(
@@ -3946,7 +3946,7 @@ private ParsedValueImpl<ParsedValue[],Font> parseFont(Term root) throws ParseExc
}

private ParsedValueImpl<ParsedValue[], TransitionDefinition> parseTransition(Term term)
throws ParseException{
throws ParseException {
ParsedValue<?, String> parsedProperty = null;
ParsedValue<ParsedValue<?, Size>, Duration> parsedDuration = null;
ParsedValue<ParsedValue<?, Size>, Duration> parsedDelay = null;
@@ -3955,7 +3955,9 @@ private ParsedValueImpl<ParsedValue[], TransitionDefinition> parseTransition(Ter
for (int i = 0; i < 4; ++i) {
if (term == null) {
break;
} else if (isEasingFunction(term.token)) {
}

if (isEasingFunction(term.token)) {
if (parsedTimingFunction != null) {
error(term, "Expected \'<single-transition-property>\' or \'<duration>\'");
}
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.util.Duration;
import java.util.Objects;

/**
* An event that signals the creation, beginning, completion and cancellation of implicit CSS transitions.
@@ -40,7 +41,7 @@
*/
public final class TransitionEvent extends Event {

private static final long serialVersionUID = 20220820L;
private static final long serialVersionUID = 1L;

/**
* Common supertype for all {@code TransitionEvent} types.
@@ -86,13 +87,14 @@ public final class TransitionEvent extends Event {
* @param eventType the event type
* @param property the {@code StyleableProperty} that is targeted by the transition
* @param elapsedTime the time that has elapsed since the transition has entered its active period
* @throws NullPointerException if {@code property} or {@code elapsedTime} is {@code null}
*/
public TransitionEvent(EventType<? extends Event> eventType,
StyleableProperty<?> property,
Duration elapsedTime) {
super(eventType);
this.property = property;
this.elapsedTime = elapsedTime;
this.property = Objects.requireNonNull(property, "property cannot be null");
this.elapsedTime = Objects.requireNonNull(elapsedTime, "elapsedTime cannot be null");
}

/**
@@ -108,7 +110,7 @@ public StyleableProperty<?> getProperty() {
* Gets the time that has elapsed since the transition has entered its active period,
* not including the time spent in the delay phase.
*
* @return the elapsed time
* @return the elapsed time, or zero if the transition has not entered its active period
*/
public Duration getElapsedTime() {
return elapsedTime;
Loading