| // Copyright (c) 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| /** |
| * @unrestricted |
| */ |
| export default class AnimationUI { |
| /** |
| * @param {!Animation.AnimationModel.Animation} animation |
| * @param {!Animation.AnimationTimeline} timeline |
| * @param {!Element} parentElement |
| */ |
| constructor(animation, timeline, parentElement) { |
| this._animation = animation; |
| this._timeline = timeline; |
| this._parentElement = parentElement; |
| |
| if (this._animation.source().keyframesRule()) { |
| this._keyframes = this._animation.source().keyframesRule().keyframes(); |
| } |
| |
| this._nameElement = parentElement.createChild('div', 'animation-name'); |
| this._nameElement.textContent = this._animation.name(); |
| |
| this._svg = parentElement.createSVGChild('svg', 'animation-ui'); |
| this._svg.setAttribute('height', Animation.AnimationUI.Options.AnimationSVGHeight); |
| this._svg.style.marginLeft = '-' + Animation.AnimationUI.Options.AnimationMargin + 'px'; |
| this._svg.addEventListener('contextmenu', this._onContextMenu.bind(this)); |
| this._activeIntervalGroup = this._svg.createSVGChild('g'); |
| UI.installDragHandle( |
| this._activeIntervalGroup, this._mouseDown.bind(this, Animation.AnimationUI.MouseEvents.AnimationDrag, null), |
| this._mouseMove.bind(this), this._mouseUp.bind(this), '-webkit-grabbing', '-webkit-grab'); |
| |
| /** @type {!Array.<{group: ?Element, animationLine: ?Element, keyframePoints: !Object.<number, !Element>, keyframeRender: !Object.<number, !Element>}>} */ |
| this._cachedElements = []; |
| |
| this._movementInMs = 0; |
| this._color = Animation.AnimationUI.Color(this._animation); |
| } |
| |
| /** |
| * @param {!Animation.AnimationModel.Animation} animation |
| * @return {string} |
| */ |
| static Color(animation) { |
| const names = Object.keys(Animation.AnimationUI.Colors); |
| const color = |
| Animation.AnimationUI.Colors[names[String.hashCode(animation.name() || animation.id()) % names.length]]; |
| return color.asString(Common.Color.Format.RGB); |
| } |
| |
| /** |
| * @return {!Animation.AnimationModel.Animation} |
| */ |
| animation() { |
| return this._animation; |
| } |
| |
| /** |
| * @param {?SDK.DOMNode} node |
| */ |
| setNode(node) { |
| this._node = node; |
| } |
| |
| /** |
| * @param {!Element} parentElement |
| * @param {string} className |
| */ |
| _createLine(parentElement, className) { |
| const line = parentElement.createSVGChild('line', className); |
| line.setAttribute('x1', Animation.AnimationUI.Options.AnimationMargin); |
| line.setAttribute('y1', Animation.AnimationUI.Options.AnimationHeight); |
| line.setAttribute('y2', Animation.AnimationUI.Options.AnimationHeight); |
| line.style.stroke = this._color; |
| return line; |
| } |
| |
| /** |
| * @param {number} iteration |
| * @param {!Element} parentElement |
| */ |
| _drawAnimationLine(iteration, parentElement) { |
| const cache = this._cachedElements[iteration]; |
| if (!cache.animationLine) { |
| cache.animationLine = this._createLine(parentElement, 'animation-line'); |
| } |
| cache.animationLine.setAttribute( |
| 'x2', |
| (this._duration() * this._timeline.pixelMsRatio() + Animation.AnimationUI.Options.AnimationMargin).toFixed(2)); |
| } |
| |
| /** |
| * @param {!Element} parentElement |
| */ |
| _drawDelayLine(parentElement) { |
| if (!this._delayLine) { |
| this._delayLine = this._createLine(parentElement, 'animation-delay-line'); |
| this._endDelayLine = this._createLine(parentElement, 'animation-delay-line'); |
| } |
| const fill = this._animation.source().fill(); |
| this._delayLine.classList.toggle('animation-fill', fill === 'backwards' || fill === 'both'); |
| const margin = Animation.AnimationUI.Options.AnimationMargin; |
| this._delayLine.setAttribute('x1', margin); |
| this._delayLine.setAttribute('x2', (this._delay() * this._timeline.pixelMsRatio() + margin).toFixed(2)); |
| const forwardsFill = fill === 'forwards' || fill === 'both'; |
| this._endDelayLine.classList.toggle('animation-fill', forwardsFill); |
| const leftMargin = Math.min( |
| this._timeline.width(), |
| (this._delay() + this._duration() * this._animation.source().iterations()) * this._timeline.pixelMsRatio()); |
| this._endDelayLine.style.transform = 'translateX(' + leftMargin.toFixed(2) + 'px)'; |
| this._endDelayLine.setAttribute('x1', margin); |
| this._endDelayLine.setAttribute( |
| 'x2', forwardsFill ? (this._timeline.width() - leftMargin + margin).toFixed(2) : |
| (this._animation.source().endDelay() * this._timeline.pixelMsRatio() + margin).toFixed(2)); |
| } |
| |
| /** |
| * @param {number} iteration |
| * @param {!Element} parentElement |
| * @param {number} x |
| * @param {number} keyframeIndex |
| * @param {boolean} attachEvents |
| */ |
| _drawPoint(iteration, parentElement, x, keyframeIndex, attachEvents) { |
| if (this._cachedElements[iteration].keyframePoints[keyframeIndex]) { |
| this._cachedElements[iteration].keyframePoints[keyframeIndex].setAttribute('cx', x.toFixed(2)); |
| return; |
| } |
| |
| const circle = |
| parentElement.createSVGChild('circle', keyframeIndex <= 0 ? 'animation-endpoint' : 'animation-keyframe-point'); |
| circle.setAttribute('cx', x.toFixed(2)); |
| circle.setAttribute('cy', Animation.AnimationUI.Options.AnimationHeight); |
| circle.style.stroke = this._color; |
| circle.setAttribute('r', Animation.AnimationUI.Options.AnimationMargin / 2); |
| |
| if (keyframeIndex <= 0) { |
| circle.style.fill = this._color; |
| } |
| |
| this._cachedElements[iteration].keyframePoints[keyframeIndex] = circle; |
| |
| if (!attachEvents) { |
| return; |
| } |
| |
| let eventType; |
| if (keyframeIndex === 0) { |
| eventType = Animation.AnimationUI.MouseEvents.StartEndpointMove; |
| } else if (keyframeIndex === -1) { |
| eventType = Animation.AnimationUI.MouseEvents.FinishEndpointMove; |
| } else { |
| eventType = Animation.AnimationUI.MouseEvents.KeyframeMove; |
| } |
| UI.installDragHandle( |
| circle, this._mouseDown.bind(this, eventType, keyframeIndex), this._mouseMove.bind(this), |
| this._mouseUp.bind(this), 'ew-resize'); |
| } |
| |
| /** |
| * @param {number} iteration |
| * @param {number} keyframeIndex |
| * @param {!Element} parentElement |
| * @param {number} leftDistance |
| * @param {number} width |
| * @param {string} easing |
| */ |
| _renderKeyframe(iteration, keyframeIndex, parentElement, leftDistance, width, easing) { |
| /** |
| * @param {!Element} parentElement |
| * @param {number} x |
| * @param {string} strokeColor |
| */ |
| function createStepLine(parentElement, x, strokeColor) { |
| const line = parentElement.createSVGChild('line'); |
| line.setAttribute('x1', x); |
| line.setAttribute('x2', x); |
| line.setAttribute('y1', Animation.AnimationUI.Options.AnimationMargin); |
| line.setAttribute('y2', Animation.AnimationUI.Options.AnimationHeight); |
| line.style.stroke = strokeColor; |
| } |
| |
| const bezier = UI.Geometry.CubicBezier.parse(easing); |
| const cache = this._cachedElements[iteration].keyframeRender; |
| if (!cache[keyframeIndex]) { |
| cache[keyframeIndex] = bezier ? parentElement.createSVGChild('path', 'animation-keyframe') : |
| parentElement.createSVGChild('g', 'animation-keyframe-step'); |
| } |
| const group = cache[keyframeIndex]; |
| group.style.transform = 'translateX(' + leftDistance.toFixed(2) + 'px)'; |
| |
| if (easing === 'linear') { |
| group.style.fill = this._color; |
| const height = InlineEditor.BezierUI.Height; |
| group.setAttribute( |
| 'd', ['M', 0, height, 'L', 0, 5, 'L', width.toFixed(2), 5, 'L', width.toFixed(2), height, 'Z'].join(' ')); |
| } else if (bezier) { |
| group.style.fill = this._color; |
| InlineEditor.BezierUI.drawVelocityChart(bezier, group, width); |
| } else { |
| const stepFunction = Animation.AnimationTimeline.StepTimingFunction.parse(easing); |
| group.removeChildren(); |
| /** @const */ const offsetMap = {'start': 0, 'middle': 0.5, 'end': 1}; |
| /** @const */ const offsetWeight = offsetMap[stepFunction.stepAtPosition]; |
| for (let i = 0; i < stepFunction.steps; i++) { |
| createStepLine(group, (i + offsetWeight) * width / stepFunction.steps, this._color); |
| } |
| } |
| } |
| |
| redraw() { |
| const maxWidth = this._timeline.width() - Animation.AnimationUI.Options.AnimationMargin; |
| |
| this._svg.setAttribute('width', (maxWidth + 2 * Animation.AnimationUI.Options.AnimationMargin).toFixed(2)); |
| this._activeIntervalGroup.style.transform = |
| 'translateX(' + (this._delay() * this._timeline.pixelMsRatio()).toFixed(2) + 'px)'; |
| |
| this._nameElement.style.transform = 'translateX(' + |
| (this._delay() * this._timeline.pixelMsRatio() + Animation.AnimationUI.Options.AnimationMargin).toFixed(2) + |
| 'px)'; |
| this._nameElement.style.width = (this._duration() * this._timeline.pixelMsRatio()).toFixed(2) + 'px'; |
| this._drawDelayLine(this._svg); |
| |
| if (this._animation.type() === 'CSSTransition') { |
| this._renderTransition(); |
| return; |
| } |
| |
| this._renderIteration(this._activeIntervalGroup, 0); |
| if (!this._tailGroup) { |
| this._tailGroup = this._activeIntervalGroup.createSVGChild('g', 'animation-tail-iterations'); |
| } |
| const iterationWidth = this._duration() * this._timeline.pixelMsRatio(); |
| let iteration; |
| for (iteration = 1; |
| iteration < this._animation.source().iterations() && iterationWidth * (iteration - 1) < this._timeline.width(); |
| iteration++) { |
| this._renderIteration(this._tailGroup, iteration); |
| } |
| while (iteration < this._cachedElements.length) { |
| this._cachedElements.pop().group.remove(); |
| } |
| } |
| |
| _renderTransition() { |
| if (!this._cachedElements[0]) { |
| this._cachedElements[0] = {animationLine: null, keyframePoints: {}, keyframeRender: {}, group: null}; |
| } |
| this._drawAnimationLine(0, this._activeIntervalGroup); |
| this._renderKeyframe( |
| 0, 0, this._activeIntervalGroup, Animation.AnimationUI.Options.AnimationMargin, |
| this._duration() * this._timeline.pixelMsRatio(), this._animation.source().easing()); |
| this._drawPoint(0, this._activeIntervalGroup, Animation.AnimationUI.Options.AnimationMargin, 0, true); |
| this._drawPoint( |
| 0, this._activeIntervalGroup, |
| this._duration() * this._timeline.pixelMsRatio() + Animation.AnimationUI.Options.AnimationMargin, -1, true); |
| } |
| |
| /** |
| * @param {!Element} parentElement |
| * @param {number} iteration |
| */ |
| _renderIteration(parentElement, iteration) { |
| if (!this._cachedElements[iteration]) { |
| this._cachedElements[iteration] = |
| {animationLine: null, keyframePoints: {}, keyframeRender: {}, group: parentElement.createSVGChild('g')}; |
| } |
| const group = this._cachedElements[iteration].group; |
| group.style.transform = |
| 'translateX(' + (iteration * this._duration() * this._timeline.pixelMsRatio()).toFixed(2) + 'px)'; |
| this._drawAnimationLine(iteration, group); |
| console.assert(this._keyframes.length > 1); |
| for (let i = 0; i < this._keyframes.length - 1; i++) { |
| const leftDistance = this._offset(i) * this._duration() * this._timeline.pixelMsRatio() + |
| Animation.AnimationUI.Options.AnimationMargin; |
| const width = this._duration() * (this._offset(i + 1) - this._offset(i)) * this._timeline.pixelMsRatio(); |
| this._renderKeyframe(iteration, i, group, leftDistance, width, this._keyframes[i].easing()); |
| if (i || (!i && iteration === 0)) { |
| this._drawPoint(iteration, group, leftDistance, i, iteration === 0); |
| } |
| } |
| this._drawPoint( |
| iteration, group, |
| this._duration() * this._timeline.pixelMsRatio() + Animation.AnimationUI.Options.AnimationMargin, -1, |
| iteration === 0); |
| } |
| |
| /** |
| * @return {number} |
| */ |
| _delay() { |
| let delay = this._animation.source().delay(); |
| if (this._mouseEventType === Animation.AnimationUI.MouseEvents.AnimationDrag || |
| this._mouseEventType === Animation.AnimationUI.MouseEvents.StartEndpointMove) { |
| delay += this._movementInMs; |
| } |
| // FIXME: add support for negative start delay |
| return Math.max(0, delay); |
| } |
| |
| /** |
| * @return {number} |
| */ |
| _duration() { |
| let duration = this._animation.source().duration(); |
| if (this._mouseEventType === Animation.AnimationUI.MouseEvents.FinishEndpointMove) { |
| duration += this._movementInMs; |
| } else if (this._mouseEventType === Animation.AnimationUI.MouseEvents.StartEndpointMove) { |
| duration -= Math.max(this._movementInMs, -this._animation.source().delay()); |
| } // Cannot have negative delay |
| return Math.max(0, duration); |
| } |
| |
| /** |
| * @param {number} i |
| * @return {number} offset |
| */ |
| _offset(i) { |
| let offset = this._keyframes[i].offsetAsNumber(); |
| if (this._mouseEventType === Animation.AnimationUI.MouseEvents.KeyframeMove && i === this._keyframeMoved) { |
| console.assert(i > 0 && i < this._keyframes.length - 1, 'First and last keyframe cannot be moved'); |
| offset += this._movementInMs / this._animation.source().duration(); |
| offset = Math.max(offset, this._keyframes[i - 1].offsetAsNumber()); |
| offset = Math.min(offset, this._keyframes[i + 1].offsetAsNumber()); |
| } |
| return offset; |
| } |
| |
| /** |
| * @param {!Animation.AnimationUI.MouseEvents} mouseEventType |
| * @param {?number} keyframeIndex |
| * @param {!Event} event |
| */ |
| _mouseDown(mouseEventType, keyframeIndex, event) { |
| if (event.buttons === 2) { |
| return false; |
| } |
| if (this._svg.enclosingNodeOrSelfWithClass('animation-node-removed')) { |
| return false; |
| } |
| this._mouseEventType = mouseEventType; |
| this._keyframeMoved = keyframeIndex; |
| this._downMouseX = event.clientX; |
| event.consume(true); |
| if (this._node) { |
| Common.Revealer.reveal(this._node); |
| } |
| return true; |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _mouseMove(event) { |
| this._movementInMs = (event.clientX - this._downMouseX) / this._timeline.pixelMsRatio(); |
| if (this._delay() + this._duration() > this._timeline.duration() * 0.8) { |
| this._timeline.setDuration(this._timeline.duration() * 1.2); |
| } |
| this.redraw(); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _mouseUp(event) { |
| this._movementInMs = (event.clientX - this._downMouseX) / this._timeline.pixelMsRatio(); |
| |
| // Commit changes |
| if (this._mouseEventType === Animation.AnimationUI.MouseEvents.KeyframeMove) { |
| this._keyframes[this._keyframeMoved].setOffset(this._offset(this._keyframeMoved)); |
| } else { |
| this._animation.setTiming(this._duration(), this._delay()); |
| } |
| |
| this._movementInMs = 0; |
| this.redraw(); |
| |
| delete this._mouseEventType; |
| delete this._downMouseX; |
| delete this._keyframeMoved; |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _onContextMenu(event) { |
| /** |
| * @param {?SDK.RemoteObject} remoteObject |
| */ |
| function showContextMenu(remoteObject) { |
| if (!remoteObject) { |
| return; |
| } |
| const contextMenu = new UI.ContextMenu(event); |
| contextMenu.appendApplicableItems(remoteObject); |
| contextMenu.show(); |
| } |
| |
| this._animation.remoteObjectPromise().then(showContextMenu); |
| event.consume(true); |
| } |
| } |
| |
| /** |
| * @enum {string} |
| */ |
| export const MouseEvents = { |
| AnimationDrag: 'AnimationDrag', |
| KeyframeMove: 'KeyframeMove', |
| StartEndpointMove: 'StartEndpointMove', |
| FinishEndpointMove: 'FinishEndpointMove' |
| }; |
| |
| export const Options = { |
| AnimationHeight: 26, |
| AnimationSVGHeight: 50, |
| AnimationMargin: 7, |
| EndpointsClickRegionSize: 10, |
| GridCanvasHeight: 40 |
| }; |
| |
| export const Colors = { |
| 'Purple': Common.Color.parse('#9C27B0'), |
| 'Light Blue': Common.Color.parse('#03A9F4'), |
| 'Deep Orange': Common.Color.parse('#FF5722'), |
| 'Blue': Common.Color.parse('#5677FC'), |
| 'Lime': Common.Color.parse('#CDDC39'), |
| 'Blue Grey': Common.Color.parse('#607D8B'), |
| 'Pink': Common.Color.parse('#E91E63'), |
| 'Green': Common.Color.parse('#0F9D58'), |
| 'Brown': Common.Color.parse('#795548'), |
| 'Cyan': Common.Color.parse('#00BCD4') |
| }; |
| |
| /* Legacy exported object */ |
| self.Animation = self.Animation || {}; |
| |
| /* Legacy exported object */ |
| Animation = Animation || {}; |
| |
| /** |
| * @constructor |
| */ |
| Animation.AnimationUI = AnimationUI; |
| |
| /** |
| * @enum {string} |
| */ |
| Animation.AnimationUI.MouseEvents = MouseEvents; |
| |
| Animation.AnimationUI.Options = Options; |
| |
| Animation.AnimationUI.Colors = Colors; |