UNPKG

chrome-devtools-frontend

Version:
245 lines (220 loc) • 7.47 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 type * as SDK from '../../core/sdk/sdk.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Lit from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import {AnimationUI} from './AnimationUI.js'; const {render, html, svg, Directives: {classMap, ref}} = Lit; const VIEW_BOX_HEIGHT = 32; const MAX_ANIMATION_LINES_TO_SHOW = 10; const MIN_ANIMATION_GROUP_DURATION = 750; interface ViewInput { isScrollDrivenAnimationGroup: boolean; isPreviewAnimationDisabled: boolean; isSelected: boolean; isPaused: boolean; isFocusable: boolean; label: string; animationGroupDuration: number; animations: SDK.AnimationModel.AnimationImpl[]; onPreviewAnimationEnd: () => void; onRemoveAnimationGroup: () => void; onSelectAnimationGroup: () => void; onCreateScreenshotPopover: () => void; onFocusNextGroup: () => void; onFocusPreviousGroup: () => void; } interface ViewOutput { replay?: () => void; focus?: () => void; } type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; const DEFAULT_VIEW: View = (input, output, target) => { const classes = classMap({ 'animation-buffer-preview': true, selected: input.isSelected, paused: input.isPaused, 'no-animation': input.isPreviewAnimationDisabled, }); const handleKeyDown = (event: KeyboardEvent): void => { switch (event.key) { case 'Backspace': case 'Delete': input.onRemoveAnimationGroup(); break; case 'ArrowLeft': case 'ArrowUp': input.onFocusPreviousGroup(); break; case 'ArrowRight': case 'ArrowDown': input.onFocusNextGroup(); } }; const renderAnimationLines = (): Lit.LitTemplate => { const timeToPixelRatio = 100 / Math.max(input.animationGroupDuration, MIN_ANIMATION_GROUP_DURATION); const viewBox = `0 0 100 ${VIEW_BOX_HEIGHT}`; const lines = input.animations.map((animation, index) => { const xStartPoint = animation.delayOrStartTime(); const xEndPoint = xStartPoint + animation.iterationDuration(); const yPoint = Math.floor(VIEW_BOX_HEIGHT / Math.max(6, input.animations.length) * index + 1); const colorForAnimation = AnimationUI.colorForAnimation(animation); // clang-format off return svg`<line x1="${xStartPoint * timeToPixelRatio}" x2="${xEndPoint * timeToPixelRatio}" y1="${yPoint}" y2="${yPoint}" style="stroke: ${colorForAnimation}"></line>`; // clang-format on }); // clang-format off return html` <svg width="100%" height="100%" viewBox=${viewBox} preserveAspectRatio="none" shape-rendering="crispEdges"> ${lines} </svg> `; // clang-format on }; // clang-format off render(html` <div class="animation-group-preview-ui"> <button jslog=${VisualLogging.item(`animations.buffer-preview${input.isScrollDrivenAnimationGroup ? '-sda' : ''}`).track({click: true})} class=${classes} role="option" aria-label=${input.label} tabindex=${input.isFocusable ? 0 : -1} @mouseover=${{ handleEvent: input.onCreateScreenshotPopover, once: true, }} @keydown=${handleKeyDown} @click=${input.onSelectAnimationGroup} @animationend=${input.onPreviewAnimationEnd} ${ref(el => { if (el instanceof HTMLElement) { output.focus = () => { el.focus(); }; } })}> <div class="animation-paused fill"></div> <devtools-icon name=${input.isScrollDrivenAnimationGroup ? 'mouse' : 'watch'} class="preview-icon"></devtools-icon> <div class="animation-buffer-preview-animation" ${ref(el => { if (el instanceof HTMLElement) { output.replay = () => { el.animate( [ {offset: 0, width: '0%', opacity: 1}, {offset: 0.9, width: '100%', opacity: 1}, {offset: 1, width: '100%', opacity: 0}, ], {duration: 200, easing: 'cubic-bezier(0, 0, 0.2, 1)'} ); }; } })}></div> ${renderAnimationLines()} </button> <button class="animation-remove-button" jslog=${VisualLogging.action('animations.remove-preview').track({click: true})} @click=${input.onRemoveAnimationGroup}> <devtools-icon name="cross"></devtools-icon> </button> </div> `, target, {host: input}); // clang-format on }; interface AnimationGroupPreviewConfig { animationGroup: SDK.AnimationModel.AnimationGroup; label: string; onRemoveAnimationGroup: () => void; onSelectAnimationGroup: () => void; onCreateScreenshotPopover: () => void; onFocusNextGroup: () => void; onFocusPreviousGroup: () => void; } export class AnimationGroupPreviewUI extends UI.Widget.Widget { #view: View; #viewOutput: ViewOutput = {}; #config: AnimationGroupPreviewConfig; #previewAnimationDisabled = false; #selected = false; #paused = false; #focusable = false; constructor(config: AnimationGroupPreviewConfig, view = DEFAULT_VIEW) { super(); this.#view = view; this.#config = config; this.requestUpdate(); } setSelected(selected: boolean): void { if (this.#selected === selected) { return; } this.#selected = selected; this.requestUpdate(); } setPaused(paused: boolean): void { if (this.#paused === paused) { return; } this.#paused = paused; this.requestUpdate(); } setFocusable(focusable: boolean): void { if (this.#focusable === focusable) { return; } this.#focusable = focusable; this.requestUpdate(); } override performUpdate(): void { this.#view( { isScrollDrivenAnimationGroup: this.#config.animationGroup.isScrollDriven(), isPreviewAnimationDisabled: this.#previewAnimationDisabled, isSelected: this.#selected, isPaused: this.#paused, isFocusable: this.#focusable, label: this.#config.label, animationGroupDuration: this.#config.animationGroup.groupDuration(), animations: this.#config.animationGroup.animations().slice(0, MAX_ANIMATION_LINES_TO_SHOW), onPreviewAnimationEnd: () => { this.#previewAnimationDisabled = true; this.requestUpdate(); }, onRemoveAnimationGroup: () => { this.#config.onRemoveAnimationGroup(); }, onSelectAnimationGroup: () => { this.#config.onSelectAnimationGroup(); }, onCreateScreenshotPopover: () => { this.#config.onCreateScreenshotPopover(); }, onFocusNextGroup: () => { this.#config.onFocusNextGroup(); }, onFocusPreviousGroup: () => { this.#config.onFocusPreviousGroup(); } }, this.#viewOutput, this.contentElement); } override focus(): void { this.#viewOutput.focus?.(); } replay(): void { this.#viewOutput.replay?.(); } }