| // 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 class BezierEditor extends UI.VBox { |
| constructor() { |
| super(true); |
| this.registerRequiredCSS('inline_editor/bezierEditor.css'); |
| this.contentElement.tabIndex = 0; |
| this.setDefaultFocusedElement(this.contentElement); |
| |
| // Preview UI |
| this._previewElement = this.contentElement.createChild('div', 'bezier-preview-container'); |
| this._previewElement.createChild('div', 'bezier-preview-animation'); |
| this._previewElement.addEventListener('click', this._startPreviewAnimation.bind(this)); |
| this._previewOnion = this.contentElement.createChild('div', 'bezier-preview-onion'); |
| this._previewOnion.addEventListener('click', this._startPreviewAnimation.bind(this)); |
| |
| this._outerContainer = this.contentElement.createChild('div', 'bezier-container'); |
| |
| // Presets UI |
| this._presetsContainer = this._outerContainer.createChild('div', 'bezier-presets'); |
| this._presetUI = new InlineEditor.BezierUI(40, 40, 0, 2, false); |
| this._presetCategories = []; |
| for (let i = 0; i < Presets.length; i++) { |
| this._presetCategories[i] = this._createCategory(Presets[i]); |
| this._presetsContainer.appendChild(this._presetCategories[i].icon); |
| } |
| |
| // Curve UI |
| this._curveUI = new InlineEditor.BezierUI(150, 250, 50, 7, true); |
| this._curve = this._outerContainer.createSVGChild('svg', 'bezier-curve'); |
| UI.installDragHandle( |
| this._curve, this._dragStart.bind(this), this._dragMove.bind(this), this._dragEnd.bind(this), 'default'); |
| |
| this._header = this.contentElement.createChild('div', 'bezier-header'); |
| const minus = this._createPresetModifyIcon(this._header, 'bezier-preset-minus', 'M 12 6 L 8 10 L 12 14'); |
| const plus = this._createPresetModifyIcon(this._header, 'bezier-preset-plus', 'M 8 6 L 12 10 L 8 14'); |
| minus.addEventListener('click', this._presetModifyClicked.bind(this, false)); |
| plus.addEventListener('click', this._presetModifyClicked.bind(this, true)); |
| this._label = this._header.createChild('span', 'source-code bezier-display-value'); |
| } |
| |
| /** |
| * @param {?UI.Geometry.CubicBezier} bezier |
| */ |
| setBezier(bezier) { |
| if (!bezier) { |
| return; |
| } |
| this._bezier = bezier; |
| this._updateUI(); |
| } |
| |
| /** |
| * @return {!UI.Geometry.CubicBezier} |
| */ |
| bezier() { |
| return this._bezier; |
| } |
| |
| /** |
| * @override |
| */ |
| wasShown() { |
| this._unselectPresets(); |
| // Check if bezier matches a preset |
| for (const category of this._presetCategories) { |
| for (let i = 0; i < category.presets.length; i++) { |
| if (this._bezier.asCSSText() === category.presets[i].value) { |
| category.presetIndex = i; |
| this._presetCategorySelected(category); |
| } |
| } |
| } |
| |
| this._updateUI(); |
| this._startPreviewAnimation(); |
| } |
| |
| _onchange() { |
| this._updateUI(); |
| this.dispatchEventToListeners(Events.BezierChanged, this._bezier.asCSSText()); |
| } |
| |
| _updateUI() { |
| const labelText = this._selectedCategory ? this._selectedCategory.presets[this._selectedCategory.presetIndex].name : |
| this._bezier.asCSSText().replace(/\s(-\d\.\d)/g, '$1'); |
| this._label.textContent = Common.UIString(labelText); |
| this._curveUI.drawCurve(this._bezier, this._curve); |
| this._previewOnion.removeChildren(); |
| } |
| |
| /** |
| * @param {!Event} event |
| * @return {boolean} |
| */ |
| _dragStart(event) { |
| this._mouseDownPosition = new UI.Geometry.Point(event.x, event.y); |
| const ui = this._curveUI; |
| this._controlPosition = new UI.Geometry.Point( |
| Number.constrain((event.offsetX - ui.radius) / ui.curveWidth(), 0, 1), |
| (ui.curveHeight() + ui.marginTop + ui.radius - event.offsetY) / ui.curveHeight()); |
| |
| const firstControlPointIsCloser = this._controlPosition.distanceTo(this._bezier.controlPoints[0]) < |
| this._controlPosition.distanceTo(this._bezier.controlPoints[1]); |
| this._selectedPoint = firstControlPointIsCloser ? 0 : 1; |
| |
| this._bezier.controlPoints[this._selectedPoint] = this._controlPosition; |
| this._unselectPresets(); |
| this._onchange(); |
| |
| event.consume(true); |
| return true; |
| } |
| |
| /** |
| * @param {number} mouseX |
| * @param {number} mouseY |
| */ |
| _updateControlPosition(mouseX, mouseY) { |
| const deltaX = (mouseX - this._mouseDownPosition.x) / this._curveUI.curveWidth(); |
| const deltaY = (mouseY - this._mouseDownPosition.y) / this._curveUI.curveHeight(); |
| const newPosition = new UI.Geometry.Point( |
| Number.constrain(this._controlPosition.x + deltaX, 0, 1), this._controlPosition.y - deltaY); |
| this._bezier.controlPoints[this._selectedPoint] = newPosition; |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _dragMove(event) { |
| this._updateControlPosition(event.x, event.y); |
| this._onchange(); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _dragEnd(event) { |
| this._updateControlPosition(event.x, event.y); |
| this._onchange(); |
| this._startPreviewAnimation(); |
| } |
| |
| /** |
| * @param {!Array<{name: string, value: string}>} presetGroup |
| * @return {!InlineEditor.BezierEditor.PresetCategory} |
| */ |
| _createCategory(presetGroup) { |
| const presetElement = createElementWithClass('div', 'bezier-preset-category'); |
| const iconElement = presetElement.createSVGChild('svg', 'bezier-preset monospace'); |
| const category = {presets: presetGroup, presetIndex: 0, icon: presetElement}; |
| this._presetUI.drawCurve(UI.Geometry.CubicBezier.parse(category.presets[0].value), iconElement); |
| iconElement.addEventListener('click', this._presetCategorySelected.bind(this, category)); |
| return category; |
| } |
| |
| /** |
| * @param {!Element} parentElement |
| * @param {string} className |
| * @param {string} drawPath |
| * @return {!Element} |
| */ |
| _createPresetModifyIcon(parentElement, className, drawPath) { |
| const icon = parentElement.createSVGChild('svg', 'bezier-preset-modify ' + className); |
| icon.setAttribute('width', 20); |
| icon.setAttribute('height', 20); |
| const path = icon.createSVGChild('path'); |
| path.setAttribute('d', drawPath); |
| return icon; |
| } |
| |
| _unselectPresets() { |
| for (const category of this._presetCategories) { |
| category.icon.classList.remove('bezier-preset-selected'); |
| } |
| delete this._selectedCategory; |
| this._header.classList.remove('bezier-header-active'); |
| } |
| |
| /** |
| * @param {!InlineEditor.BezierEditor.PresetCategory} category |
| * @param {!Event=} event |
| */ |
| _presetCategorySelected(category, event) { |
| if (this._selectedCategory === category) { |
| return; |
| } |
| this._unselectPresets(); |
| this._header.classList.add('bezier-header-active'); |
| this._selectedCategory = category; |
| this._selectedCategory.icon.classList.add('bezier-preset-selected'); |
| this.setBezier(UI.Geometry.CubicBezier.parse(category.presets[category.presetIndex].value)); |
| this._onchange(); |
| this._startPreviewAnimation(); |
| if (event) { |
| event.consume(true); |
| } |
| } |
| |
| /** |
| * @param {boolean} intensify |
| * @param {!Event} event |
| */ |
| _presetModifyClicked(intensify, event) { |
| if (!this._selectedCategory) { |
| return; |
| } |
| |
| const length = this._selectedCategory.presets.length; |
| this._selectedCategory.presetIndex = (this._selectedCategory.presetIndex + (intensify ? 1 : -1) + length) % length; |
| this.setBezier( |
| UI.Geometry.CubicBezier.parse(this._selectedCategory.presets[this._selectedCategory.presetIndex].value)); |
| this._onchange(); |
| this._startPreviewAnimation(); |
| } |
| |
| _startPreviewAnimation() { |
| if (this._previewAnimation) { |
| this._previewAnimation.cancel(); |
| } |
| |
| const animationDuration = 1600; |
| const numberOnionSlices = 20; |
| |
| const keyframes = [ |
| {offset: 0, transform: 'translateX(0px)', easing: this._bezier.asCSSText(), opacity: 1}, |
| {offset: 0.9, transform: 'translateX(218px)', opacity: 1}, |
| {offset: 1, transform: 'translateX(218px)', opacity: 0} |
| ]; |
| this._previewAnimation = this._previewElement.animate(keyframes, animationDuration); |
| this._previewOnion.removeChildren(); |
| for (let i = 0; i <= numberOnionSlices; i++) { |
| const slice = this._previewOnion.createChild('div', 'bezier-preview-animation'); |
| const player = slice.animate( |
| [{transform: 'translateX(0px)', easing: this._bezier.asCSSText()}, {transform: 'translateX(218px)'}], |
| {duration: animationDuration, fill: 'forwards'}); |
| player.pause(); |
| player.currentTime = animationDuration * i / numberOnionSlices; |
| } |
| } |
| } |
| |
| /** @enum {symbol} */ |
| export const Events = { |
| BezierChanged: Symbol('BezierChanged') |
| }; |
| |
| export const Presets = [ |
| [ |
| {name: 'ease-in-out', value: 'ease-in-out'}, {name: 'In Out · Sine', value: 'cubic-bezier(0.45, 0.05, 0.55, 0.95)'}, |
| {name: 'In Out · Quadratic', value: 'cubic-bezier(0.46, 0.03, 0.52, 0.96)'}, |
| {name: 'In Out · Cubic', value: 'cubic-bezier(0.65, 0.05, 0.36, 1)'}, |
| {name: 'Fast Out, Slow In', value: 'cubic-bezier(0.4, 0, 0.2, 1)'}, |
| {name: 'In Out · Back', value: 'cubic-bezier(0.68, -0.55, 0.27, 1.55)'} |
| ], |
| [ |
| {name: 'Fast Out, Linear In', value: 'cubic-bezier(0.4, 0, 1, 1)'}, {name: 'ease-in', value: 'ease-in'}, |
| {name: 'In · Sine', value: 'cubic-bezier(0.47, 0, 0.75, 0.72)'}, |
| {name: 'In · Quadratic', value: 'cubic-bezier(0.55, 0.09, 0.68, 0.53)'}, |
| {name: 'In · Cubic', value: 'cubic-bezier(0.55, 0.06, 0.68, 0.19)'}, |
| {name: 'In · Back', value: 'cubic-bezier(0.6, -0.28, 0.74, 0.05)'} |
| ], |
| [ |
| {name: 'ease-out', value: 'ease-out'}, {name: 'Out · Sine', value: 'cubic-bezier(0.39, 0.58, 0.57, 1)'}, |
| {name: 'Out · Quadratic', value: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'}, |
| {name: 'Out · Cubic', value: 'cubic-bezier(0.22, 0.61, 0.36, 1)'}, |
| {name: 'Linear Out, Slow In', value: 'cubic-bezier(0, 0, 0.2, 1)'}, |
| {name: 'Out · Back', value: 'cubic-bezier(0.18, 0.89, 0.32, 1.28)'} |
| ] |
| ]; |
| |
| /* Legacy exported object */ |
| self.InlineEditor = self.InlineEditor || {}; |
| |
| /* Legacy exported object */ |
| InlineEditor = InlineEditor || {}; |
| |
| /** @constructor */ |
| InlineEditor.BezierEditor = BezierEditor; |
| |
| InlineEditor.BezierEditor.Events = Events; |
| InlineEditor.BezierEditor.Presets = Presets; |
| |
| /** @typedef {{presets: !Array.<{name: string, value: string}>, icon: !Element, presetIndex: number}} */ |
| InlineEditor.BezierEditor.PresetCategory; |