UNPKG

chrome-devtools-frontend

Version:
288 lines (253 loc) 11.4 kB
// 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. import * as Common from '../../../../core/common/common.js'; import * as Platform from '../../../../core/platform/platform.js'; import * as UI from '../../legacy.js'; import bezierEditorStyles from './bezierEditor.css.js'; import {BezierUI} from './BezierUI.js'; export class BezierEditor extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) { private bezierInternal: UI.Geometry.CubicBezier; private previewElement: HTMLElement; private readonly previewOnion: HTMLElement; private readonly outerContainer: HTMLElement; private selectedCategory: PresetCategory|null; private readonly presetsContainer: HTMLElement; private readonly presetUI: BezierUI; private readonly presetCategories: PresetCategory[]; private readonly curveUI: BezierUI; private readonly curve: Element; private readonly header: HTMLElement; private label: HTMLElement; private mouseDownPosition?: UI.Geometry.Point; private controlPosition?: UI.Geometry.Point; private selectedPoint?: number; private previewAnimation?: Animation; constructor(bezier: UI.Geometry.CubicBezier) { super(true); this.bezierInternal = bezier; 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.selectedCategory = null; this.presetsContainer = this.outerContainer.createChild('div', 'bezier-presets'); this.presetUI = new 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 BezierUI(150, 250, 50, 7, true); this.curve = UI.UIUtils.createSVGChild(this.outerContainer, 'svg', 'bezier-curve'); UI.UIUtils.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'); } setBezier(bezier: UI.Geometry.CubicBezier): void { if (!bezier) { return; } this.bezierInternal = bezier; this.updateUI(); } bezier(): UI.Geometry.CubicBezier { return this.bezierInternal; } wasShown(): void { this.registerCSSFiles([bezierEditorStyles]); 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.bezierInternal.asCSSText() === category.presets[i].value) { category.presetIndex = i; this.presetCategorySelected(category); } } } this.updateUI(); this.startPreviewAnimation(); } private onchange(): void { this.updateUI(); this.dispatchEventToListeners(Events.BezierChanged, this.bezierInternal.asCSSText()); } private updateUI(): void { const labelText = this.selectedCategory ? this.selectedCategory.presets[this.selectedCategory.presetIndex].name : this.bezierInternal.asCSSText().replace(/\s(-\d\.\d)/g, '$1'); this.label.textContent = labelText; this.curveUI.drawCurve(this.bezierInternal, this.curve); this.previewOnion.removeChildren(); } private dragStart(event: MouseEvent): boolean { this.mouseDownPosition = new UI.Geometry.Point(event.x, event.y); const ui = this.curveUI; this.controlPosition = new UI.Geometry.Point( Platform.NumberUtilities.clamp((event.offsetX - ui.radius) / ui.curveWidth(), 0, 1), (ui.curveHeight() + ui.marginTop + ui.radius - event.offsetY) / ui.curveHeight()); const firstControlPointIsCloser = this.controlPosition.distanceTo(this.bezierInternal.controlPoints[0]) < this.controlPosition.distanceTo(this.bezierInternal.controlPoints[1]); this.selectedPoint = firstControlPointIsCloser ? 0 : 1; this.bezierInternal.controlPoints[this.selectedPoint] = this.controlPosition; this.unselectPresets(); this.onchange(); event.consume(true); return true; } private updateControlPosition(mouseX: number, mouseY: number): void { if (this.mouseDownPosition === undefined || this.controlPosition === undefined || this.selectedPoint === undefined) { return; } const deltaX = (mouseX - this.mouseDownPosition.x) / this.curveUI.curveWidth(); const deltaY = (mouseY - this.mouseDownPosition.y) / this.curveUI.curveHeight(); const newPosition = new UI.Geometry.Point( Platform.NumberUtilities.clamp(this.controlPosition.x + deltaX, 0, 1), this.controlPosition.y - deltaY); this.bezierInternal.controlPoints[this.selectedPoint] = newPosition; } private dragMove(event: MouseEvent): void { this.updateControlPosition(event.x, event.y); this.onchange(); } private dragEnd(event: MouseEvent): void { this.updateControlPosition(event.x, event.y); this.onchange(); this.startPreviewAnimation(); } private createCategory(presetGroup: { name: string, value: string, }[]): PresetCategory { const presetElement = document.createElement('div'); presetElement.classList.add('bezier-preset-category'); const iconElement = UI.UIUtils.createSVGChild(presetElement, '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; } private createPresetModifyIcon(parentElement: Element, className: string, drawPath: string): Element { const icon = UI.UIUtils.createSVGChild(parentElement, 'svg', 'bezier-preset-modify ' + className); icon.setAttribute('width', '20'); icon.setAttribute('height', '20'); const path = UI.UIUtils.createSVGChild(icon, 'path'); path.setAttribute('d', drawPath); return icon; } private unselectPresets(): void { for (const category of this.presetCategories) { category.icon.classList.remove('bezier-preset-selected'); } this.selectedCategory = null; this.header.classList.remove('bezier-header-active'); } private presetCategorySelected(category: PresetCategory, event?: Event): void { 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'); const newBezier = UI.Geometry.CubicBezier.parse(category.presets[category.presetIndex].value); if (newBezier) { this.setBezier(newBezier); this.onchange(); this.startPreviewAnimation(); } if (event) { event.consume(true); } } private presetModifyClicked(intensify: boolean, _event: Event): void { if (this.selectedCategory === null) { return; } const length = this.selectedCategory.presets.length; this.selectedCategory.presetIndex = (this.selectedCategory.presetIndex + (intensify ? 1 : -1) + length) % length; const newBezier = UI.Geometry.CubicBezier.parse(this.selectedCategory.presets[this.selectedCategory.presetIndex].value); if (newBezier) { this.setBezier(newBezier); this.onchange(); this.startPreviewAnimation(); } } private startPreviewAnimation(): void { if (this.previewAnimation) { this.previewAnimation.cancel(); } const animationDuration = 1600; const numberOnionSlices = 20; const keyframes = [ {offset: 0, transform: 'translateX(0px)', easing: this.bezierInternal.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.bezierInternal.asCSSText()}, {transform: 'translateX(218px)'}], {duration: animationDuration, fill: 'forwards'}); player.pause(); player.currentTime = animationDuration * i / numberOnionSlices; } } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { BezierChanged = 'BezierChanged', } export type EventTypes = { [Events.BezierChanged]: string, }; 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)'}, ], ]; export interface PresetCategory { presets: { name: string, value: string, }[]; icon: Element; presetIndex: number; }