Bug 1431573 - Part 8: Implement progress bar in keyframes. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Mon, 12 Mar 2018 13:08:01 +0900
changeset 766119 e6a8508fd23fe1668a86a3a08d87f6b4a6241ddc
parent 766118 172a1d9adca77696ce2f50173c500b6482893e56
child 766120 b3a76122631fa682a9f659c6ea2228985983c123
push id102230
push userbmo:dakatsuka@mozilla.com
push dateMon, 12 Mar 2018 09:02:24 +0000
reviewersgl
bugs1431573
milestone60.0a1
Bug 1431573 - Part 8: Implement progress bar in keyframes. r?gl MozReview-Commit-ID: GE5Of8VklAJ
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
devtools/client/inspector/animation/components/AnimatedPropertyListHeader.js
devtools/client/inspector/animation/components/AnimationDetailContainer.js
devtools/client/inspector/animation/components/App.js
devtools/client/inspector/animation/components/KeyframesProgressBar.js
devtools/client/inspector/animation/components/moz.build
devtools/client/inspector/animation/reducers/animations.js
devtools/client/themes/animation.css
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -27,27 +27,30 @@ const {
 class AnimationInspector {
   constructor(inspector, win) {
     this.inspector = inspector;
     this.win = win;
 
     this.addAnimationsCurrentTimeListener =
       this.addAnimationsCurrentTimeListener.bind(this);
     this.getAnimatedPropertyMap = this.getAnimatedPropertyMap.bind(this);
+    this.getAnimationsCurrentTime = this.getAnimationsCurrentTime.bind(this);
     this.getComputedStyle = this.getComputedStyle.bind(this);
     this.getNodeFromActor = this.getNodeFromActor.bind(this);
     this.removeAnimationsCurrentTimeListener =
       this.removeAnimationsCurrentTimeListener.bind(this);
     this.rewindAnimationsCurrentTime = this.rewindAnimationsCurrentTime.bind(this);
     this.selectAnimation = this.selectAnimation.bind(this);
     this.setAnimationsCurrentTime = this.setAnimationsCurrentTime.bind(this);
     this.setAnimationsPlaybackRate = this.setAnimationsPlaybackRate.bind(this);
     this.setAnimationsPlayState = this.setAnimationsPlayState.bind(this);
     this.setDetailVisibility = this.setDetailVisibility.bind(this);
     this.simulateAnimation = this.simulateAnimation.bind(this);
+    this.simulateAnimationForKeyframesProgressBar =
+      this.simulateAnimationForKeyframesProgressBar.bind(this);
     this.toggleElementPicker = this.toggleElementPicker.bind(this);
     this.update = this.update.bind(this);
     this.onAnimationsCurrentTimeUpdated = this.onAnimationsCurrentTimeUpdated.bind(this);
     this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
     this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
     this.onSidebarResized = this.onSidebarResized.bind(this);
     this.onSidebarSelect = this.onSidebarSelect.bind(this);
 
@@ -66,27 +69,29 @@ class AnimationInspector {
     const {
       onHideBoxModelHighlighter,
     } = this.inspector.getPanel("boxmodel").getComponentProps();
 
     const {
       addAnimationsCurrentTimeListener,
       emit: emitEventForTest,
       getAnimatedPropertyMap,
+      getAnimationsCurrentTime,
       getComputedStyle,
       getNodeFromActor,
       isAnimationsRunning,
       removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
       selectAnimation,
       setAnimationsCurrentTime,
       setAnimationsPlaybackRate,
       setAnimationsPlayState,
       setDetailVisibility,
       simulateAnimation,
+      simulateAnimationForKeyframesProgressBar,
       toggleElementPicker,
     } = this;
 
     const target = this.inspector.target;
     this.animationsFront = new AnimationsFront(target.client, target.form);
 
     this.animationsCurrentTimeListeners = [];
     this.isCurrentTimeSet = false;
@@ -97,30 +102,32 @@ class AnimationInspector {
         key: "newanimationinspector",
         store: this.inspector.store
       },
       App(
         {
           addAnimationsCurrentTimeListener,
           emitEventForTest,
           getAnimatedPropertyMap,
+          getAnimationsCurrentTime,
           getComputedStyle,
           getNodeFromActor,
           isAnimationsRunning,
           onHideBoxModelHighlighter,
           onShowBoxModelHighlighterForNode,
           removeAnimationsCurrentTimeListener,
           rewindAnimationsCurrentTime,
           selectAnimation,
           setAnimationsCurrentTime,
           setAnimationsPlaybackRate,
           setAnimationsPlayState,
           setDetailVisibility,
           setSelectedNode,
           simulateAnimation,
+          simulateAnimationForKeyframesProgressBar,
           toggleElementPicker,
         }
       )
     );
     this.provider = provider;
 
     this.inspector.selection.on("new-node-front", this.update);
     this.inspector.sidebar.on("newanimationinspector-selected", this.onSidebarSelect);
@@ -141,16 +148,21 @@ class AnimationInspector {
       this.simulatedAnimation = null;
     }
 
     if (this.simulatedElement) {
       this.simulatedElement.remove();
       this.simulatedElement = null;
     }
 
+    if (this.simulatedAnimationForKeyframesProgressBar) {
+      this.simulatedAnimationForKeyframesProgressBar.cancel();
+      this.simulatedAnimationForKeyframesProgressBar = null;
+    }
+
     this.stopAnimationsCurrentTimeTimer();
 
     this.inspector = null;
     this.win = null;
   }
 
   get state() {
     return this.inspector.store.getState().animations;
@@ -194,16 +206,20 @@ class AnimationInspector {
       });
 
       animatedPropertyMap.set(name, keyframes);
     }
 
     return animatedPropertyMap;
   }
 
+  getAnimationsCurrentTime() {
+    return this.currentTime;
+  }
+
   /**
    * Return the computed style of the specified property after setting the given styles
    * to the simulated element.
    *
    * @param {String} property
    *        CSS property name (e.g. text-align).
    * @param {Object} styles
    *        Map of CSS property name and value.
@@ -233,16 +249,18 @@ class AnimationInspector {
   /**
    * This method should call when the current time is changed.
    * Then, dispatches the current time to listeners that are registered
    * by addAnimationsCurrentTimeListener.
    *
    * @param {Number} currentTime
    */
   onAnimationsCurrentTimeUpdated(currentTime) {
+    this.currentTime = currentTime;
+
     for (const listener of this.animationsCurrentTimeListeners) {
       listener(currentTime);
     }
   }
 
   onElementPickerStarted() {
     this.inspector.store.dispatch(updateElementPickerEnabled(true));
   }
@@ -355,16 +373,37 @@ class AnimationInspector {
     }
 
     this.simulatedAnimation.effect =
       new this.win.KeyframeEffect(targetEl, keyframes, effectTiming);
 
     return this.simulatedAnimation;
   }
 
+  /**
+   * Returns a simulatable efect timing animation for the keyframes progress bar.
+   * The returned animation is implementing Animation interface of Web Animation API.
+   * https://drafts.csswg.org/web-animations/#the-animation-interface
+   *
+   * @param {Object} effectTiming
+   *        e.g. { duration: 1000, fill: "both" }
+   * @return {Animation}
+   *         https://drafts.csswg.org/web-animations/#the-animation-interface
+   */
+  simulateAnimationForKeyframesProgressBar(effectTiming) {
+    if (!this.simulatedAnimationForKeyframesProgressBar) {
+      this.simulatedAnimationForKeyframesProgressBar = new this.win.Animation();
+    }
+
+    this.simulatedAnimationForKeyframesProgressBar.effect =
+      new this.win.KeyframeEffect(null, null, effectTiming);
+
+    return this.simulatedAnimationForKeyframesProgressBar;
+  }
+
   stopAnimationsCurrentTimeTimer() {
     if (this.currentTimeTimer) {
       this.currentTimeTimer.destroy();
       this.currentTimeTimer = null;
     }
   }
 
   startAnimationsCurrentTimeTimer() {
@@ -405,25 +444,16 @@ class AnimationInspector {
     });
 
     await Promise.all(promises);
   }
 
   updateState(animations) {
     this.stopAnimationsCurrentTimeTimer();
 
-    // If number of displayed animations is one, we select the animation automatically.
-    // But if selected animation is in given animations, ignores.
-    const selectedAnimation = this.state.selectedAnimation;
-
-    if (!selectedAnimation ||
-        !animations.find(animation => animation.actorID === selectedAnimation.actorID)) {
-      this.selectAnimation(animations.length === 1 ? animations[0] : null);
-    }
-
     this.inspector.store.dispatch(updateAnimations(animations));
 
     if (hasPlayingAnimation(animations)) {
       this.startAnimationsCurrentTimeTimer();
     }
   }
 }
 
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyListContainer.js
@@ -9,38 +9,57 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const AnimatedPropertyList = createFactory(require("./AnimatedPropertyList"));
 const AnimatedPropertyListHeader = createFactory(require("./AnimatedPropertyListHeader"));
 
 class AnimatedPropertyListContainer extends PureComponent {
   static get propTypes() {
     return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getAnimationsCurrentTime: PropTypes.func.isRequired,
       getComputedStyle: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
+      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
     };
   }
 
   render() {
     const {
+      addAnimationsCurrentTimeListener,
       animation,
       emitEventForTest,
       getAnimatedPropertyMap,
+      getAnimationsCurrentTime,
       getComputedStyle,
+      removeAnimationsCurrentTimeListener,
       simulateAnimation,
+      simulateAnimationForKeyframesProgressBar,
+      timeScale,
     } = this.props;
 
     return dom.div(
       {
         className: `animated-property-list-container ${ animation.state.type }`
       },
-      AnimatedPropertyListHeader(),
+      AnimatedPropertyListHeader(
+        {
+          addAnimationsCurrentTimeListener,
+          animation,
+          getAnimationsCurrentTime,
+          removeAnimationsCurrentTimeListener,
+          simulateAnimationForKeyframesProgressBar,
+          timeScale,
+        }
+      ),
       AnimatedPropertyList(
         {
           animation,
           emitEventForTest,
           getAnimatedPropertyMap,
           getComputedStyle,
           simulateAnimation,
         }
--- a/devtools/client/inspector/animation/components/AnimatedPropertyListHeader.js
+++ b/devtools/client/inspector/animation/components/AnimatedPropertyListHeader.js
@@ -1,23 +1,55 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
+const KeyframesProgressBar = createFactory(require("./KeyframesProgressBar"));
 const KeyframesProgressTickList = createFactory(require("./KeyframesProgressTickList"));
 
 class AnimatedPropertyListHeader extends PureComponent {
+  static get propTypes() {
+    return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+      animation: PropTypes.object.isRequired,
+      getAnimationsCurrentTime: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
+    };
+  }
+
   render() {
+    const {
+      addAnimationsCurrentTimeListener,
+      animation,
+      getAnimationsCurrentTime,
+      removeAnimationsCurrentTimeListener,
+      simulateAnimationForKeyframesProgressBar,
+      timeScale,
+    } = this.props;
+
     return dom.div(
       {
         className: "animated-property-list-header devtools-toolbar"
       },
-      KeyframesProgressTickList()
+      KeyframesProgressTickList(),
+      KeyframesProgressBar(
+        {
+          addAnimationsCurrentTimeListener,
+          animation,
+          getAnimationsCurrentTime,
+          removeAnimationsCurrentTimeListener,
+          simulateAnimationForKeyframesProgressBar,
+          timeScale,
+        }
+      )
     );
   }
 }
 
 module.exports = AnimatedPropertyListHeader;
--- a/devtools/client/inspector/animation/components/AnimationDetailContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationDetailContainer.js
@@ -11,33 +11,43 @@ const PropTypes = require("devtools/clie
 
 const AnimationDetailHeader = createFactory(require("./AnimationDetailHeader"));
 const AnimatedPropertyListContainer =
   createFactory(require("./AnimatedPropertyListContainer"));
 
 class AnimationDetailContainer extends PureComponent {
   static get propTypes() {
     return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animation: PropTypes.object.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getAnimationsCurrentTime: PropTypes.func.isRequired,
       getComputedStyle: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       setDetailVisibility: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
+      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
     };
   }
 
   render() {
     const {
+      addAnimationsCurrentTimeListener,
       animation,
       emitEventForTest,
       getAnimatedPropertyMap,
+      getAnimationsCurrentTime,
       getComputedStyle,
+      removeAnimationsCurrentTimeListener,
       setDetailVisibility,
       simulateAnimation,
+      simulateAnimationForKeyframesProgressBar,
+      timeScale,
     } = this.props;
 
     return dom.div(
       {
         className: "animation-detail-container"
       },
       animation ?
         AnimationDetailHeader(
@@ -46,21 +56,26 @@ class AnimationDetailContainer extends P
             setDetailVisibility,
           }
         )
       :
         null,
       animation ?
         AnimatedPropertyListContainer(
           {
+            addAnimationsCurrentTimeListener,
             animation,
             emitEventForTest,
             getAnimatedPropertyMap,
+            getAnimationsCurrentTime,
             getComputedStyle,
+            removeAnimationsCurrentTimeListener,
             simulateAnimation,
+            simulateAnimationForKeyframesProgressBar,
+            timeScale,
           }
         )
       :
         null
     );
   }
 }
 
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -18,58 +18,62 @@ const SplitBox = createFactory(require("
 class App extends PureComponent {
   static get propTypes() {
     return {
       addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
       detailVisibility: PropTypes.bool.isRequired,
       emitEventForTest: PropTypes.func.isRequired,
       getAnimatedPropertyMap: PropTypes.func.isRequired,
+      getAnimationsCurrentTime: PropTypes.func.isRequired,
       getComputedStyle: PropTypes.func.isRequired,
       getNodeFromActor: PropTypes.func.isRequired,
       onHideBoxModelHighlighter: PropTypes.func.isRequired,
       onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
       removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
       rewindAnimationsCurrentTime: PropTypes.func.isRequired,
       selectAnimation: PropTypes.func.isRequired,
       setAnimationsCurrentTime: PropTypes.func.isRequired,
       setAnimationsPlaybackRate: PropTypes.func.isRequired,
       setAnimationsPlayState: PropTypes.func.isRequired,
       setDetailVisibility: PropTypes.func.isRequired,
       setSelectedNode: PropTypes.func.isRequired,
       simulateAnimation: PropTypes.func.isRequired,
+      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
       timeScale: PropTypes.object.isRequired,
       toggleElementPicker: PropTypes.func.isRequired,
     };
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.props.animations.length !== 0 || nextProps.animations.length !== 0;
   }
 
   render() {
     const {
       addAnimationsCurrentTimeListener,
       animations,
       detailVisibility,
       emitEventForTest,
       getAnimatedPropertyMap,
+      getAnimationsCurrentTime,
       getComputedStyle,
       getNodeFromActor,
       onHideBoxModelHighlighter,
       onShowBoxModelHighlighterForNode,
       removeAnimationsCurrentTimeListener,
       rewindAnimationsCurrentTime,
       selectAnimation,
       setAnimationsCurrentTime,
       setAnimationsPlaybackRate,
       setAnimationsPlayState,
       setDetailVisibility,
       setSelectedNode,
       simulateAnimation,
+      simulateAnimationForKeyframesProgressBar,
       timeScale,
       toggleElementPicker,
     } = this.props;
 
     return dom.div(
       {
         id: "animation-container",
         className: detailVisibility ? "animation-detail-visible" : "",
@@ -85,21 +89,26 @@ class App extends PureComponent {
             setAnimationsPlaybackRate,
             setAnimationsPlayState,
           }
         ),
         SplitBox({
           className: "animation-container-splitter",
           endPanel: AnimationDetailContainer(
             {
+              addAnimationsCurrentTimeListener,
               emitEventForTest,
               getAnimatedPropertyMap,
+              getAnimationsCurrentTime,
               getComputedStyle,
+              removeAnimationsCurrentTimeListener,
               setDetailVisibility,
               simulateAnimation,
+              simulateAnimationForKeyframesProgressBar,
+              timeScale,
             }
           ),
           endPanelControl: true,
           initialHeight: "50%",
           splitterSize: 1,
           startPanel: AnimationListContainer(
             {
               addAnimationsCurrentTimeListener,
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/KeyframesProgressBar.js
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+class KeyframesProgressBar extends PureComponent {
+  static get propTypes() {
+    return {
+      addAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+      animation: PropTypes.object.isRequired,
+      getAnimationsCurrentTime: PropTypes.func.isRequired,
+      removeAnimationsCurrentTimeListener: PropTypes.func.isRequired,
+      simulateAnimationForKeyframesProgressBar: PropTypes.func.isRequired,
+      timeScale: PropTypes.object.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.onCurrentTimeUpdated = this.onCurrentTimeUpdated.bind(this);
+
+    this.state = {
+      // offset of the position for the progress bar
+      offset: 0,
+    };
+  }
+
+  componentDidMount() {
+    const { addAnimationsCurrentTimeListener } = this.props;
+
+    this.element = ReactDOM.findDOMNode(this);
+    this.setupAnimation(this.props);
+    addAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const { getAnimationsCurrentTime } = nextProps;
+
+    this.setupAnimation(nextProps);
+    this.onCurrentTimeUpdated(getAnimationsCurrentTime());
+  }
+
+  componentWillUnmount() {
+    const { removeAnimationsCurrentTimeListener } = this.props;
+
+    removeAnimationsCurrentTimeListener(this.onCurrentTimeUpdated);
+    this.element = null;
+    this.simulatedAnimation = null;
+  }
+
+  onCurrentTimeUpdated(currentTime) {
+    const {
+      animation,
+      timeScale,
+    } = this.props;
+    const {
+      playbackRate,
+      previousStartTime = 0,
+    } = animation.state;
+
+    this.simulatedAnimation.currentTime =
+      (timeScale.minStartTime + currentTime - previousStartTime) * playbackRate;
+    const offset = this.element.offsetWidth *
+                   this.simulatedAnimation.effect.getComputedTiming().progress;
+
+    this.setState({ offset });
+  }
+
+  setupAnimation(props) {
+    const {
+      animation,
+      simulateAnimationForKeyframesProgressBar,
+    } = props;
+
+    if (this.simulatedAnimation) {
+      this.simulatedAnimation.cancel();
+    }
+
+    const timing = Object.assign({}, animation.state, {
+      iterations: animation.state.iterationCount || Infinity
+    });
+
+    this.simulatedAnimation = simulateAnimationForKeyframesProgressBar(timing);
+  }
+
+  render() {
+    const { offset } = this.state;
+
+    return dom.div(
+      {
+        className: "keyframes-progress-bar-area devtools-toolbar",
+      },
+      dom.div(
+        {
+          className: "keyframes-progress-bar",
+          style: {
+            transform: `translateX(${ offset }px)`,
+          },
+        }
+      )
+    );
+  }
+}
+
+module.exports = KeyframesProgressBar;
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -22,15 +22,16 @@ DevToolsModules(
     'AnimationTarget.js',
     'AnimationTimelineTickItem.js',
     'AnimationTimelineTickList.js',
     'AnimationToolbar.js',
     'App.js',
     'CurrentTimeLabel.js',
     'CurrentTimeScrubber.js',
     'CurrentTimeScrubberController.js',
+    'KeyframesProgressBar.js',
     'KeyframesProgressTickItem.js',
     'KeyframesProgressTickList.js',
     'NoAnimationPanel.js',
     'PauseResumeButton.js',
     'PlaybackRateSelector.js',
     'RewindButton.js',
 )
--- a/devtools/client/inspector/animation/reducers/animations.js
+++ b/devtools/client/inspector/animation/reducers/animations.js
@@ -23,18 +23,29 @@ const INITIAL_STATE = {
     height: 0,
     width: 0,
   },
   timeScale: null,
 };
 
 const reducers = {
   [UPDATE_ANIMATIONS](state, { animations }) {
+    let detailVisibility = state.detailVisibility;
+    let selectedAnimation = state.selectedAnimation;
+
+    if (!state.selectedAnimation ||
+        !animations.find(animation => animation.actorID === selectedAnimation.actorID)) {
+      selectedAnimation = animations.length === 1 ? animations[0] : null;
+      detailVisibility = !!selectedAnimation;
+    }
+
     return Object.assign({}, state, {
       animations,
+      detailVisibility,
+      selectedAnimation,
       timeScale: new TimeScale(animations),
     });
   },
 
   [UPDATE_DETAIL_VISIBILITY](state, { detailVisibility }) {
     return Object.assign({}, state, {
       detailVisibility
     });
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -9,16 +9,17 @@
   --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
   --fast-track-image: url("images/animation-fast-track.svg");
   --fill-color-cssanimation: var(--theme-contrast-background);
   --fill-color-csstransition: var(--theme-highlight-blue);
   --fill-color-scriptanimation: var(--theme-graphs-green);
   --graph-right-offset: 10px;
   --keyframe-marker-shadow-color: #c4c4c4;
   --pause-image: url(chrome://devtools/skin/images/pause.svg);
+  --progress-bar-color: #909090;
   --resume-image: url(chrome://devtools/skin/images/play.svg);
   --rewind-image: url(chrome://devtools/skin/images/rewind.svg);
   --scrubber-color: #dd00a9;
   --sidebar-width: 200px;
   --stroke-color-cssanimation: var(--theme-highlight-lightorange);
   --stroke-color-csstransition: var(--theme-highlight-bluegrey);
   --stroke-color-scriptanimation: var(--theme-highlight-green);
   --tick-line-style: 0.5px solid rgba(128, 136, 144, 0.5);
@@ -351,46 +352,80 @@ select.playback-rate-selector.devtools-b
   background-image: url(chrome://devtools/skin/images/close.svg);
 }
 
 /* Animated Property List Container */
 .animated-property-list-container {
   display: flex;
   flex: 1;
   flex-direction: column;
-  overflow-y: auto;
+  overflow: hidden;
 }
 
 /* Animated Property List Header */
 .animated-property-list-header {
-  display: flex;
-  justify-content: flex-end;
+  display: grid;
+  grid-template-columns: var(--sidebar-width) calc(100% - var(--sidebar-width) - var(--graph-right-offset)) var(--graph-right-offset);
   padding: 0;
 }
 
 /* Keyframes Progress Tick List */
 .keyframes-progress-tick-list {
-  margin-right: var(--graph-right-offset);
-  position: absolute;
-  width: calc(100% - var(--sidebar-width) - var(--graph-right-offset));
+  grid-column: 2 / 3;
+  position: relative;
 }
 
 .keyframes-progress-tick-item {
   height: 100vh;
   position: absolute;
 }
 
 .keyframes-progress-tick-item.left {
   border-left: var(--tick-line-style);
 }
 
 .keyframes-progress-tick-item.right {
   border-right: var(--tick-line-style);
 }
 
+/* Keyframes Progress Bar */
+.keyframes-progress-bar-area {
+  background: none;
+  grid-column: 2 / 3;
+  padding: 0;
+  pointer-events: none;
+  position: relative;
+}
+
+.keyframes-progress-bar {
+  height: 100vh;
+  position: absolute;
+  z-index: 1;
+}
+
+.keyframes-progress-bar::before {
+  border-left: 5px solid transparent;
+  border-right: 5px solid transparent;
+  border-top: 5px solid var(--progress-bar-color);
+  content: "";
+  left: -5px;
+  position: absolute;
+  top: 0;
+  width: 0;
+}
+
+.keyframes-progress-bar::after {
+  border-left: 1px solid var(--progress-bar-color);
+  content: "";
+  height: 100%;
+  position: absolute;
+  top: 0;
+  width: 0;
+}
+
 /* Animated Property List */
 .animated-property-list {
   flex: 1;
   list-style-type: none;
   margin: 0;
   overflow-y: auto;
   padding: 0;
 }