import { LitElement, html, customElement, property, css, query, CSSResult } from 'lit-element'; import { InkingCanvas } from './inking-canvas'; import { InkingToolbarButtonStyles } from './inking-toolbar-button-styles'; import * as Colors from './colors'; import * as Utils from './utils'; @customElement('inking-toolbar') export class InkingToolbar extends LitElement { // properties for toolbar and its dropdowns @property({type: String}) orientation: string = ""; @property({type: String, attribute: "vertical"}) verticalAlignment: string = ""; @property({type: String, attribute: "horizontal"}) horizontalAlignment: string = ""; @query('#toolbar-container') private toolbarContainer: HTMLElement; @query('#tool-container') private toolContainer: HTMLElement; @property({type: NodeList}) private tools: Array; @query("#customized-toolbar-selection") private customizedToolbarSelection: HTMLDivElement; @query("#default-toolbar-selection") private defaultToolbarSelection: HTMLDivElement; @property({type: Object}) private toolFocus = 0; @property({type: Number}) private penPencilFocus = 0; @property({type: Number}) private highlighterFocus = 0; @property({type: HTMLButtonElement}) private selectedTool: Element; @property({type: CustomEvent}) private toolChangedEvent: CustomEvent = new CustomEvent("tool-changed"); @property({type: CustomEvent}) private colorChangedEvent: CustomEvent = new CustomEvent("color-changed"); @query('#dropdown-container') private dropdownContainer: HTMLElement; @property({type: HTMLDivElement}) private selectedDropdown: HTMLDivElement; @query('.ink-dropdown') private inkDropdown: HTMLDivElement; @query('.more-options-dropdown') private moreOptionsDropdown: HTMLDivElement; @query('.ink-dropdown .title') private inkDropdownTitle: HTMLElement; @property({type: HTMLDivElement}) private selectedCircle: HTMLDivElement; @query('#erase-all') private eraseAllBtn: HTMLButtonElement; @query('.pen-pencil.palette') private penPencilPalette: HTMLElement; @query('.highlighter.palette') private highlighterPalette: HTMLElement; @query('#use-slider-size') private sliderCheckbox: HTMLInputElement; @query('.checkbox-track') private sliderCheckboxTrack: HTMLInputElement; @query('.on-text') private onText: HTMLElement; @query('.off-text') private offText: HTMLElement; @query('.slider') private slider: HTMLInputElement; @query("#slider-tooltip") private sliderTooltip: HTMLSpanElement; private readonly defaultSliderSize = "24"; private readonly defaultSliderMin = "1"; private readonly defaultSliderMax = "48"; private readonly highlighterSliderSize = "50"; private readonly highlighterSliderMin= "20"; private readonly highlighterSliderMax = "80"; @query('.sineCanvas') private sineCanvas: HTMLCanvasElement; @property({ type: CanvasRenderingContext2D }) private sineContext: CanvasRenderingContext2D; @property({type: Boolean}) private isWaitingToDrawSineCanvas: boolean = false; @query("#snackbar") private snackbar: HTMLDivElement; // access colors used in toolbar private static colors: Map = Colors.getColors(); // properties to influence connected inking canvas @property({type: CSSResult}) private selectedPenColor: CSSResult = Colors.black; @property({type: CSSResult}) private selectedPenColorName: string = 'black'; @property({type: Number}) private selectedPenSize: number = parseInt(this.defaultSliderSize); @property({type: CSSResult}) private selectedPencilColor: CSSResult = Colors.black; @property({type: CSSResult}) private selectedPencilColorName: string = 'black'; @property({type: Number}) private selectedPencilSize: number = parseInt(this.defaultSliderSize); @property({type: CSSResult}) private selectedHighlighterColor: CSSResult = Colors.yellow; @property({type: CSSResult}) private selectedHighlighterColorName: string = 'yellow'; @property({type: Number}) private selectedHighlighterSize: number = parseInt(this.highlighterSliderSize); @property({type: Number}) private eraserSize: number = parseInt(this.defaultSliderSize); @property({type: String, attribute: "canvas"}) targetInkingCanvas: string = ""; @property({type: InkingCanvas}) private inkingCanvas: InkingCanvas; constructor() { super(); } render() { return html `
`; } firstUpdated() { // add any (last) detected inking canvas with matching name (TODO: handle multiple) this.connectCanvas(); // set toolbar layout to developer's choice this.setOrientation(); this.setVerticalAlignment(); this.setHorizontalAlignment(); // enable low-latency if possible this.sineContext = Utils.getLowLatencyContext(this.sineCanvas, "sine") // set canvas to use pointer event sizing by default this.slider.disabled = true; this.sliderCheckbox.checked = false; this.sliderCheckbox.addEventListener('change', () => this.toggleSliderCheckbox(), false); // support keyboard navigation for slider checkbox and handle, and dropdown container this.sliderCheckbox.addEventListener("keydown", function(e: KeyboardEvent) { if (e.keyCode === 13) { // enter/return key this.click(); } }, false); this.slider.addEventListener("keydown", () => function(e: KeyboardEvent) { if (e.keyCode === 37) { // left arrow key this.value -= 1; } else if (e.keyCode === 39) { // right arrow key this.value += 1; } }), false; const outerThis = this; this.toolbarContainer.addEventListener("keydown", function(e: KeyboardEvent) { if (e.keyCode === 9) { // tab key outerThis.dropdownContainer.classList.add("tabbing-focus"); } }), false; this.toolbarContainer.addEventListener("click", function() { outerThis.dropdownContainer.classList.remove("tabbing-focus"); }), false; // draw example stroke for ink dropdowns this.isWaitingToDrawSineCanvas = true; Utils.runAsynchronously( () => { this.drawSineCanvas(); console.log("Sine canvas drawn for first time"); }); } // expose ability to check active tool name getCurrentToolName() { return (this.selectedTool) ? this.selectedTool.id : ""; } // expose ability to get stroke color, size, & style getCurrentStrokeColor() { switch (this.selectedTool.id) { case "pen" : return this.selectedPenColor.toString(); break; case "pencil" : return this.selectedPencilColor.toString(); break; case "highlighter" : return this.selectedHighlighterColor.toString(); break; case "eraser" : return Colors.white.toString(); break; default: console.log("Could not find color value for selected utensil"); break; } } getCurrentStrokeColorName() { switch (this.selectedTool.id) { case "pen" : return this.selectedPenColorName; break; case "pencil" : return this.selectedPencilColorName; break; case "highlighter" : return this.selectedHighlighterColorName; break; case "eraser" : return "white"; break; default: console.log("Could not find color name for selected utensil"); break; } } getCurrentStrokeSize() { switch (this.selectedTool.id) { case "pen" : return this.selectedPenSize; break; case "pencil" : return this.selectedPencilSize; break; case "highlighter" : return this.selectedHighlighterSize; break; case "eraser" : return this.eraserSize; break; default: console.log("Could not find stroke size for selected utensil"); break; } } private setCurrentStrokeColor(color: CSSResult) { switch (this.selectedTool.id) { case "pen" : this.selectedPenColor = color; break; case "pencil" : this.selectedPencilColor = color; break; case "highlighter" : this.selectedHighlighterColor = color; break; case "eraser" : return Colors.white; break; default: console.log("Could not set color value for selected utensil"); break; } } private setCurrentStrokeColorName(colorName: string) { switch (this.selectedTool.id) { case "pen" : this.selectedPenColorName = colorName; break; case "pencil" : this.selectedPencilColorName = colorName; break; case "highlighter" : this.selectedHighlighterColorName = colorName; break; case "eraser" : break; default: console.log("Could not set color name for selected utensil"); break; } } private setCurrentStrokeSize() { switch (this.selectedTool.id) { case "pen" : this.selectedPenSize = parseInt(this.slider.value); break; case "pencil" : this.selectedPencilSize = parseInt(this.slider.value); break; case "highlighter" : this.selectedHighlighterSize = parseInt(this.slider.value); break; case "eraser" : this.eraserSize = parseInt(this.slider.value); break; default: console.log("Could not set stroke size for selected utensil"); break; } } // expose ability to trigger additional sine canvas redraws requestDrawSineCanvas() { if (!this.isWaitingToDrawSineCanvas) { this.isWaitingToDrawSineCanvas = true; } } private connectCanvas() { // find matching inking canvas const possibleCanvas = this.shadowRoot.host.parentElement; if ((possibleCanvas).name === this.targetInkingCanvas) { this.inkingCanvas = possibleCanvas; } // attach events to matching inking canvas if (this.inkingCanvas) { // make toolbar appear when connected to an inking canvas this.toolbarContainer.classList.add("show"); // hide dropdown once inking starts this.inkingCanvas.addEventListener('inking-started', () => { this.closeToolbar(); }, false); // redraw example stroke with new size when inking canvas resizes this.inkingCanvas.addEventListener('inking-canvas-drawn', () => { this.requestDrawSineCanvas(); }, false); // provide visual status of copy clicks this.inkingCanvas.addEventListener('inking-canvas-copied', ( e: CustomEvent ) => { this.flashSnackbar(e.detail.message); }, false); // set up default toolbar if no tool selection was specified if (!this.tools && this.children.length === 0) { this.tools = Array.from(this.toolContainer.querySelectorAll("button")); this.defaultToolbarSelection.classList.add("show"); this.defaultToolbarSelection.addEventListener("keydown", (e) => this.handleToolSwitchingByKeyboard(e, this.tools), false); // make sure toolbar fits inside canvas if (!this.toolbarContainer.classList.contains("vertical-orientation")) { this.inkingCanvas.setMinWidth(this.toolbarContainer.offsetWidth); } } // provide keyboard navigation for toolbar colors let penPencilColors = Array.from(this.penPencilPalette.querySelectorAll('.circle')); this.penPencilPalette.addEventListener("keydown", (e) => this.handlePenPencilSwitchingByKeyboard(e, penPencilColors), false); let highlighterColors = Array.from(this.highlighterPalette.querySelectorAll('.circle')); this.highlighterPalette.addEventListener("keydown", (e) => this.handleHighlighterSwitchingByKeyboard(e, highlighterColors), false); } } // expose method to children tools so they can add themselves to the toolbar once they load addToolbarButton(inkingToolbarButton: any, customElementName: string) { inkingToolbarButton = this.querySelector(customElementName); if (inkingToolbarButton) { let tool = inkingToolbarButton.shadowRoot.querySelector("button"); if (tool) { if (!this.tools) this.tools = new Array(); this.tools.push(tool); if (this.children.length === this.tools.length) { // done welcoming last tool, so set toolbar layout to developer's choice this.setOrientation(); this.setVerticalAlignment(); this.setHorizontalAlignment(); this.customizedToolbarSelection.addEventListener("keydown", (e) => this.handleToolSwitchingByKeyboard(e, this.tools), false); // make sure toolbar fits inside canvas if (!this.toolbarContainer.classList.contains("vertical-orientation")) { this.inkingCanvas.setMinWidth(this.toolbarContainer.offsetWidth); } } } } } private closeToolbar() { Utils.hideElementIfVisible(this.selectedDropdown); Utils.hideElementIfVisible(this.dropdownContainer); this.blurToolButtonFocus(); if (this.selectedTool) Utils.hideElementIfVisible(this.selectedTool); if (this.selectedTool.id === "more") { this.resetToolbarToLastUsedUtensil(); } } private resetToolbarToLastUsedUtensil() { let lastUsedUtensil = this.getLastUsedUtensil(); this.selectedTool = lastUsedUtensil; this.selectedDropdown = this.inkDropdown; this.selectedTool.classList.add(this.getCurrentStrokeColorName()); this.selectedTool.classList.add("clicked"); } private getLastUsedUtensil() { let name = this.inkingCanvas.getStrokeStyle(); for (let tool in this.tools) { if (this.tools[tool].id === name) return this.tools[tool]; } } // TODO: find better to pass in & update Focus (index) properties instead of code duplication for keyboard navigation private handleToolSwitchingByKeyboard(e: KeyboardEvent, tabs: HTMLButtonElement[]) { // react to only left, up, right, or down arrow keys if (e.keyCode > 36 && e.keyCode < 41) { // unfocus whatever is currently selected tabs[this.toolFocus].setAttribute("tabindex", "-1"); // right or down arrow key, respectively if (e.keyCode === 39 || e.keyCode === 40) { this.toolFocus++; // if we're at the end, go to the start if (this.toolFocus >= tabs.length) { this.toolFocus = 0; } // left or up arrow key, respectively } else if (e.keyCode === 37 || e.keyCode === 38) { this.toolFocus--; // if we're at the start, move to the end if (this.toolFocus < 0) { this.toolFocus = tabs.length - 1; } } // set focus for new selection tabs[this.toolFocus].setAttribute("tabindex", "0"); tabs[this.toolFocus].focus(); } } private handlePenPencilSwitchingByKeyboard(e: KeyboardEvent, tabs: HTMLButtonElement[]) { // react to only left, up, right, or down arrow keys if (e.keyCode > 36 && e.keyCode < 41) { // unfocus whatever is currently selected tabs[this.penPencilFocus].setAttribute("tabindex", "-1"); // right or down arrow key, respectively if (e.keyCode === 39 || e.keyCode === 40) { this.penPencilFocus++; // if we're at the end, go to the start if (this.penPencilFocus >= tabs.length) { this.penPencilFocus = 0; } // left or up arrow key, respectively } else if (e.keyCode === 37 || e.keyCode === 38) { this.penPencilFocus--; // if we're at the start, move to the end if (this.penPencilFocus < 0) { this.penPencilFocus = tabs.length - 1; } } // set focus for new selection tabs[this.penPencilFocus].setAttribute("tabindex", "0"); tabs[this.penPencilFocus].focus(); } } private handleHighlighterSwitchingByKeyboard(e: KeyboardEvent, tabs: HTMLButtonElement[]) { // react to only left, up, right, or down arrow keys if (e.keyCode > 36 && e.keyCode < 41) { // unfocus whatever is currently selected tabs[this.highlighterFocus].setAttribute("tabindex", "-1"); // right or down arrow key, respectively if (e.keyCode === 39 || e.keyCode === 40) { this.highlighterFocus++; // if we're at the end, go to the start if (this.highlighterFocus >= tabs.length) { this.highlighterFocus = 0; } // left or up arrow key, respectively } else if (e.keyCode === 37 || e.keyCode === 38) { this.highlighterFocus--; // if we're at the start, move to the end if (this.highlighterFocus < 0) { this.highlighterFocus = tabs.length - 1; } } // set focus for new selection tabs[this.highlighterFocus].setAttribute("tabindex", "0"); tabs[this.highlighterFocus].focus(); } } private setOrientation() { // default choice is "horizontal" if (this.orientation === "vertical") { if (this.toolbarContainer) this.toolbarContainer.classList.add("vertical-orientation"); if (this.toolContainer) this.toolContainer.classList.add("vertical-orientation"); if (this.dropdownContainer) this.dropdownContainer.classList.add("vertical-orientation"); if (this.tools) { this.tools.forEach(tool => { tool.classList.add('vertical-orientation'); }); } } else { if (this.tools) { this.tools.forEach(tool => { tool.classList.add('horizontal-orientation'); }); } } } private setVerticalAlignment() { // default choice/setting is "top" switch (this.verticalAlignment) { case "": break; case "top": break; case "center": if (this.toolbarContainer) this.toolbarContainer.classList.add("vertical-center"); if (this.dropdownContainer) this.dropdownContainer.classList.add("vertical-center"); break; case "bottom": if (this.toolbarContainer) this.toolbarContainer.classList.add("bottom"); if (this.dropdownContainer) this.dropdownContainer.classList.add("bottom"); if (this.tools) { this.tools.forEach(tool => { tool.classList.add("bottom"); }); } break; default: console.log("Could not set vertical toolbar alignment"); } } private setHorizontalAlignment() { // default choice/setting is "left" switch (this.horizontalAlignment) { case "": if (this.tools) { this.tools.forEach(tool => { tool.classList.add("left"); }); } break; case "left": this.tools.forEach(tool => { tool.classList.add("left"); }); break; case "center": if (this.toolbarContainer) this.toolbarContainer.classList.add("horizontal-center"); if (this.dropdownContainer) this.dropdownContainer.classList.add("horizontal-center"); if (this.tools) { this.tools.forEach(tool => { tool.classList.add("center"); }); } break; case "right": if (this.toolbarContainer) this.toolbarContainer.classList.add("right"); if (this.dropdownContainer)this.dropdownContainer.classList.add("right"); if (this.tools) { this.tools.forEach(tool => { tool.classList.add("right"); }); } break; default: console.log("Could not set horizontal toolbar alignment"); } } private async drawSineCanvas() { if (this.isWaitingToDrawSineCanvas && this.sineCanvas.classList.contains("show")) { // toggle semaphore to prevent unnecessary redraws this.isWaitingToDrawSineCanvas = false; // resize sine canvas with high resolution let rect = this.sineCanvas.getBoundingClientRect(); if (rect.height !== 0 && rect.width !== 0 ) { this.sineCanvas.height = rect.height * devicePixelRatio; this.sineCanvas.width = rect.width * devicePixelRatio; } // define stroke size and pen color for new sine wave // TODO: find better way to scale strokeWidth based on different inking canvas and sine canvas aspect ratios let aspectRatioCorrection = 1.15; let strokeWidth = parseInt(this.slider.value) * this.inkingCanvas.getScale() * aspectRatioCorrection; this.sineContext.lineWidth = strokeWidth; this.sineContext.strokeStyle = this.getCurrentStrokeColor(); // clear canvas for new sine wave this.sineContext.clearRect(0, 0, this.sineCanvas.width, this.sineCanvas.height); this.sineContext.fillStyle = Colors.colorPaletteBackground.toString(); this.sineContext.fillRect(0, 0, this.sineCanvas.width, this.sineCanvas.height); // make the stroke points round this.sineContext.lineCap = 'round'; this.sineContext.lineJoin = 'round'; let w = this.sineCanvas.width; // determine vertical center of sine wave in canvas let h = this.sineCanvas.height/2; let a = h/2; // amplitude (height of wave) let f = 1; // frequency (1 wave) // formula for sine wave is: // y = a * sin( ( 2 * pi * (frequency/timePeriod) * x ) + offsetFromOrigin) // where timePeriod is width of canvas & offsetFromOrigin is 0 // and x & y are the coordinates we want to draw on // start drawing the sine wave at an horizontal offset so it doesn't appear clipped off let x = strokeWidth; // vertically center start of the output by subtracting // the sine wave y calcualation from h (half the canvas height) let previousY = h - (a * Math.sin(2 * Math.PI * f/w * x)); let currentY: number; // calibrate sine wave rotation calcuations to center results in canvas let rotationDegrees = 354; let offsetY = a/2 + (360 - rotationDegrees); let offsetX = -5 * devicePixelRatio; let strokesDrawn = 0; // draw the sine wave until just before the canvas ends to avoid clipping off end for (let i = strokeWidth/2 + 1; i < w - strokeWidth/2 - 1; i++) { this.sineContext.beginPath();                               let rotatedX1 = (x * Math.cos(rotationDegrees * Math.PI/180)) - (previousY * Math.sin(rotationDegrees * Math.PI/180));                 let rotatedY1 = (previousY * Math.cos(rotationDegrees * Math.PI/180)) + (x * Math.sin(rotationDegrees * Math.PI/180));                 // this.sineContext.moveTo(x,previousY);                 this.sineContext.moveTo(rotatedX1 + offsetX, rotatedY1 + offsetY);                 x = i;                 currentY = h - (a * Math.sin(2 * Math.PI * f/w * x));                                  let rotatedX2 = (x * Math.cos(rotationDegrees * Math.PI/180)) - (currentY * Math.sin(rotationDegrees * Math.PI/180));                 let rotatedY2 = (currentY * Math.cos(rotationDegrees * Math.PI/180)) + (x * Math.sin(rotationDegrees * Math.PI/180));                 // this.sineContext.lineTo(x, currentY);                 this.sineContext.lineTo(rotatedX2 + offsetX, rotatedY2 + offsetY);                 previousY = currentY;                 if (this.selectedTool.id === "pencil") { this.sineContext.fillStyle = this.sineContext.strokeStyle; // Utils.drawPencilStroke(this.sineContext, x-1, x, previousY, currentY);                     Utils.drawPencilStroke(this.sineContext, rotatedX1 + offsetX, rotatedX2 + offsetX, rotatedY1 + offsetY, rotatedY2 + offsetY);                 } else {                     this.sineContext.stroke();                 }                 strokesDrawn++; } // console.log("sineCanvas strokes drawn: " + strokesDrawn); } // start & continue sine wave drawing loop Utils.runAsynchronously( () => { requestAnimationFrame(async () => this.drawSineCanvas()); }); } private clickedTool(e: Event) { let tool = e.target; if (tool.localName === "svg") { tool = tool.parentElement; } else if (tool.parentElement.localName === "svg") { tool = tool.parentElement.parentElement; } else if (tool.parentElement.parentElement.localName === "svg") { tool = tool.parentElement.parentElement.parentElement; } else if (tool.id === "") { tool = tool.shadowRoot.firstElementChild; if (tool.id === "copy") { this.clickedCopy(); return; } else if (tool.id === "save") { this.clickedSave(); return; } } console.log(tool.id + " button clicked!"); this.updateSelectedTool(tool); } private clickedEraseAll(e: Event) { let eraseAll = (e.target); console.log(eraseAll.id + " has been clicked!"); Utils.runAsynchronously( () => { this.inkingCanvas.eraseAll(); }); } private clickedColor(event: Event) { // find clicked color grid element through its class let selectedCircle = (event.target); let colorClass = selectedCircle.className.replace("clicked", "").replace("circle", "").replace("tooltip", "").trim(); // get color string from css color let colorName = Utils.toCamelCase(colorClass); let backgroundColor = InkingToolbar.colors.get(colorName); this.changeInkingColor(backgroundColor, colorName); this.updateSliderColor(colorClass); this.updateCheckboxColor(); if (this.sineCanvas) { this.requestDrawSineCanvas(); } this.updateSelectedColor(selectedCircle); // let any connected tool components know to update their color if applicable if (!this.defaultToolbarSelection.classList.contains("show")) { this.dispatchEvent(this.colorChangedEvent); } } private clickedCopy() { try { if (this.inkingCanvas) { this.inkingCanvas.copyCanvasContents(); } else { console.error("Cannot copy - inking canvas not connected"); this.flashSnackbar("Could not copy canvas to clipboard :("); } } catch (err) { console.error(err); } } private clickedSave() { try { if (this.inkingCanvas) { this.inkingCanvas.saveCanvasContents(); } else { console.error("Cannot save - inking canvas not connected"); } this.closeToolbar(); } catch (err) { console.error(err); } } private clickedImport() { try { if (this.inkingCanvas) { this.inkingCanvas.importCanvasContents(); } else { console.error("Cannot import - inking canvas not connected") } this.closeToolbar(); } catch (err) { console.log(err); } } private flashSnackbar(message: string) { this.snackbar.textContent = message; this.snackbar.classList.add("show"); setTimeout(() => { this.snackbar.classList.remove("show"); }, 3000); } private isUtensil(tool: string) { return (tool === "pen" || tool === "pencil" || tool === "highlighter" || tool === "eraser"); } private updateSelectedTool(selectedTool: Element) { if (selectedTool !== this.selectedTool) { this.updateToolDropdown(selectedTool); if (this.isUtensil(selectedTool.id)) { this.changeInkingColor(); this.inkingCanvas.setStrokeStyle(this.selectedTool.id); } } else { // this.blurToolButtonFocus(); this.selectedDropdown.classList.toggle("show"); this.dropdownContainer.classList.toggle("show"); selectedTool.classList.toggle("show"); if (!selectedTool.classList.contains("show")) this.resetToolbarToLastUsedUtensil(); } } private blurToolButtonFocus() { if (document.activeElement instanceof HTMLButtonElement) { (document.activeElement).blur(); } else if (document.activeElement.shadowRoot && document.activeElement.shadowRoot.activeElement instanceof HTMLButtonElement) { (document.activeElement.shadowRoot.activeElement).blur(); } } private updateToolDropdown(el: Element) { let utensilName = el.id; if (utensilName === "highlighter") { this.inkDropdownTitle.classList.add("show"); this.togglePalette(this.penPencilPalette, this.highlighterPalette); Utils.hideElementIfVisible(this.eraseAllBtn); } else if (utensilName === "eraser") { Utils.hideElementIfVisible(this.inkDropdownTitle); Utils.hideElementIfVisible(this.penPencilPalette); Utils.hideElementIfVisible(this.highlighterPalette); if (!this.eraseAllBtn.classList.contains("show")) this.eraseAllBtn.classList.add("show"); } else if (utensilName === "pen" || utensilName === "pencil") { this.inkDropdownTitle.classList.add("show"); this.togglePalette(this.highlighterPalette, this.penPencilPalette); Utils.hideElementIfVisible(this.eraseAllBtn); } let newDropdown = (utensilName === "more") ? this.moreOptionsDropdown : this.inkDropdown; this.toggleDropdown(newDropdown, el === this.selectedTool); this.toggleActiveTool(el); } private toggleActiveTool(lastClickedTool: Element) { if (this.selectedTool !== lastClickedTool) { if (this.selectedTool && this.selectedTool.classList.contains('clicked')) { // remove the color class of deselected utensil if (this.isUtensil(this.selectedTool.id)) this.selectedTool.classList.remove(Utils.toDash(this.getCurrentStrokeColorName())); this.selectedTool.classList.remove('clicked'); this.selectedTool.classList.remove('show'); } if (this.defaultToolbarSelection.classList.contains("show")) { if (this.selectedTool) { this.selectedTool.setAttribute("tabindex", "-1"); this.selectedTool.setAttribute("aria-pressed", "-1"); } else { for (let i = 0; i < this.tools.length; i++) { if (this.tools[i] !== lastClickedTool) { this.tools[i].setAttribute("tabindex", "-1"); this.tools[i].setAttribute("aria-pressed", "-1"); } } } } this.selectedTool = lastClickedTool; this.selectedTool.classList.add('clicked'); this.selectedTool.classList.add('show'); if (this.defaultToolbarSelection.classList.contains("show")) { this.selectedTool.setAttribute("tabindex", "0"); this.selectedTool.setAttribute("aria-pressed", "0"); } else { // inform any connected tool components so they can update their states for accessibility this.dispatchEvent(this.toolChangedEvent); } if (this.isUtensil(this.selectedTool.id)) { // use the css friendly color class name with dashes let colorName = Utils.toDash(this.getCurrentStrokeColorName()); this.selectedTool.classList.add(colorName); let selectedCircle: HTMLDivElement; if (this.selectedTool.id === "highlighter") { selectedCircle = this.inkDropdown.querySelector('.ink-dropdown .highlighter .' + colorName); } else { selectedCircle = this.inkDropdown.querySelector('.ink-dropdown .pen-pencil .' + colorName); } this.updateSelectedColor(selectedCircle); // update slider appearance to match saved utensil settings this.updateSliderColor(colorName); this.updateSliderRange(); this.updateSliderSize(); this.updateCheckboxColor(); } } } private toggleSliderCheckbox() { this.updateCheckboxColor(); this.slider.disabled = !this.slider.disabled; this.sineCanvas.classList.toggle("show"); this.slider.classList.toggle("show"); this.onText.classList.toggle("show"); this.offText.classList.toggle("show"); this.changeStrokeSize(); } private togglePalette(old: HTMLElement, current?: HTMLElement) { Utils.hideElementIfVisible(old); if (current && !current.classList.contains("show")) { current.classList.add("show"); } } private toggleDropdown(selectedDropdown: HTMLDivElement, isLastElementClicked: boolean) { if (this.selectedDropdown && this.selectedDropdown === selectedDropdown) { if (this.selectedDropdown.classList.contains("show") && isLastElementClicked) { this.selectedDropdown.classList.remove("show"); this.dropdownContainer.classList.remove("show"); this.selectedDropdown.setAttribute("tabindex", "-1"); } else { this.selectedDropdown.classList.add("show"); this.dropdownContainer.classList.add("show"); this.selectedDropdown.setAttribute("tabindex", "0"); } } else { if (this.selectedDropdown && this.selectedDropdown !== selectedDropdown) { this.selectedDropdown.classList.remove("show"); this.selectedDropdown.setAttribute("tabindex", "-1"); } this.selectedDropdown = selectedDropdown; this.selectedDropdown.classList.add("show"); this.dropdownContainer.classList.add("show"); this.selectedDropdown.setAttribute("tabindex", "0"); } } private changeInkingColor(color?: CSSResult, colorName?: string) { if (this.inkingCanvas) { if (color) this.setCurrentStrokeColor(color); if (colorName) this.setCurrentStrokeColorName(colorName); if (this.selectedTool && this.selectedTool.classList.contains('clicked')) { // remove the color class let length = this.selectedTool.classList.length; if (this.selectedTool.classList[length-1] !== "clicked" && this.selectedTool.classList[length-1] !== "show") { this.selectedTool.classList.remove(this.selectedTool.classList[length-1]); } else if (this.selectedTool.classList[length-2] !== "clicked" && this.selectedTool.classList[length-2] !== "show") { this.selectedTool.classList.remove(this.selectedTool.classList[length-2]); } else { this.selectedTool.classList.remove(this.selectedTool.classList[length-3]); } // use the css friendly color class name with dashes let modifiedColorName = Utils.toDash(this.getCurrentStrokeColorName()); this.selectedTool.classList.add(modifiedColorName); } this.inkingCanvas.setStrokeColor(this.getCurrentStrokeColor()); } } private changeStrokeSize() { if (this.inkingCanvas) { if (this.slider.disabled) { this.inkingCanvas.setStrokeSize(-1); } else { this.setCurrentStrokeSize(); this.updateSliderTooltip(); this.inkingCanvas.setStrokeSize(this.getCurrentStrokeSize()); if (this.sineCanvas) { this.requestDrawSineCanvas(); } } } } private updateSelectedColor(selectedCircle: HTMLDivElement) { if (this.selectedCircle !== selectedCircle) { if (this.selectedCircle && this.selectedCircle.classList.contains("clicked")) { this.selectedCircle.classList.remove("clicked"); this.selectedCircle.setAttribute("tabindex", "-1"); this.selectedCircle.setAttribute("aria-pressed", "-1"); } this.selectedCircle = selectedCircle; this.selectedCircle.classList.add("clicked"); this.selectedCircle.setAttribute("tabindex", "0"); this.selectedCircle.setAttribute("aria-pressed", "0") } } private updateCheckboxColor() { if (this.sliderCheckboxTrack) { let color = Utils.toDash(this.getCurrentStrokeColorName()); if (this.sliderCheckboxTrack.classList.length > 1) { this.sliderCheckboxTrack.classList.remove(this.sliderCheckboxTrack.classList[1]); if (this.sliderCheckbox.checked) this.sliderCheckboxTrack.classList.add(color); } else if (this.sliderCheckbox.checked) { this.sliderCheckboxTrack.classList.add(color); } } } private updateSliderColor(colorClass: string) { if (this.slider) { if (this.slider.classList.length > 1) { if (this.slider.classList[1] === "show") { this.slider.classList.remove(this.slider.classList[2]); } else { this.slider.classList.remove(this.slider.classList[1]); } } this.slider.classList.add(colorClass); } } private updateSliderSize() { if (this.slider) { this.slider.value = this.getCurrentStrokeSize().toString(); this.changeStrokeSize(); } } private updateSliderTooltip() { let value = this.slider.value; let min = parseInt(this.slider.min); let max = parseInt(this.slider.max); let newValue = ((parseInt(value) - min) / (max - min)) * 100; this.sliderTooltip.innerHTML = value; this.sliderTooltip.style.left = `calc(${newValue}% + (${8 - newValue * 0.15}px))`; } private updateSliderRange() { if (this.isUtensil(this.selectedTool.id)) { if (this.selectedTool.id === "highlighter") { this.slider.min = this.highlighterSliderMin; this.slider.max = this.highlighterSliderMax; } else { this.slider.min = this.defaultSliderMin; this.slider.max = this.defaultSliderMax; } } } static get styles() { return [ InkingToolbarButtonStyles, css ` #toolbar-container { position: absolute; display: none; margin: 6px; white-space: nowrap; } #toolbar-container.show { display: inline-block; } #toolbar-container.vertical-center { bottom: 50%; transform: translateY(50%); } #toolbar-container.bottom { bottom: 0; margin-bottom: 8px; // TODO: update to fit dev specified canvas border width } #toolbar-container.horizontal-center { right: 50%; transform: translateX(50%); } #toolbar-container.vertical-center.horizontal-center { transform: translate(50%, 50%); } #toolbar-container.right { position: fixed; right: 0; } #tool-container { background-color: ${Colors.white}; margin: 2px 2px 0px 2px; display: inline-block; font-size: 0; // remove children's inline-block spacing } #tool-container.vertical-orientation { margin: 2px 0px 2px 2px; /* no gap between right of tool and dropdown */ border-bottom: 2px solid ${Colors.white}; border-right: 0px solid ${Colors.white}; } #default-toolbar-selection { display: none; } #default-toolbar-selection.show { display: block; } #dropdown-container { background-color: ${Colors.colorPaletteBackground}; width: 300px; position: absolute; border: none; margin: -2px 2px 2px; } #dropdown-container.show { box-sizing: border-box; border: 2px solid ${Colors.gray}; border-radius: 2px; } #dropdown-container:focus { outline: none; -webkit-appearance: none; } /* make focus-visible workaround for Safari */ #dropdown-container.tabbing-focus:focus { outline: auto; } #dropdown-container:focus-visible { outline: auto; } #dropdown-container.vertical-orientation { display: inline-block; margin-top: 2px; margin-left: -2px; } #dropdown-container.vertical-orientation.show { min-height: 200px; } #dropdown-container.right { right: 0; margin-right: 2px; // TODO: update to fit dev specified canvas border width } #dropdown-container.vertical-orientation.right { margin-right: 48px; // TODO: update to fit dev specified canvas border width } #dropdown-container.vertical-center { top: 100%; } #dropdown-container.vertical-orientation.vertical-center { top: 0; } #dropdown-container.bottom { bottom: 0; margin-top: 0; margin-bottom: 48px; // TODO: update to fit dev specified canvas border width } #dropdown-container.vertical-orientation.bottom { margin-bottom: 4px; } // @media screen and (max-width: 400px) { // #dropdown-container { // width: 270px; // } // #dropdown-container.vertical-orientation { // width: 220px; // } // } .ink-dropdown { display: none; padding: 10px; padding-bottom: 14px; font-family: sans-serif; font-size: 16px; outline: none; } .ink-dropdown.show { display: block; } .palette { display: none; grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); grid-auto-rows: minmax(25px, auto); justify-items: center; align-items: center; justify-content: center; align-content: center; // border: 5px solid transparent; } .palette.show { display: grid; } .sineCanvas { height: 100px; width: 100%; max-height: 150px; background-color: transparent; padding-left: 0; padding-right: 0; padding-bottom: 17px; /* TODO: find better to prevent slider cutoff */ margin-left: auto; margin-right: auto; display: none; } .sineCanvas.show { display: block; } .checkbox-wrapper { position: relative; width: 65px; height: 30px; } .checkbox-wrapper input { width: 65px; height: 30px; margin: 0 auto; position: absolute; opacity: 0; } .checkbox-wrapper input:focus-visible { opacity: 1; outline: 2px solid currentColor; } .checkbox-text { position: relative; top: 7px; margin-left: 75px; white-space: nowrap; } .checkbox-track { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: ${Colors.darkGray}; border-radius: 20px; transition: all 0.2s ease; color: ${Colors.white}; } .checkbox-track::after { position: absolute; content: ""; width: 20px; height: 20px; background-color: ${Colors.white}; border-radius: 50%; border: 4px solid ${Colors.darkGray}; top: 1px; left: 1px; transition: all 0.2s ease; } input:checked + .checkbox-track.white::after { background-color: ${Colors.silver}; border-color: ${Colors.white}; } .checkbox-track.black::after { border-color: ${Colors.black}; } .checkbox-track.silver::after { border-color: ${Colors.silver}; } .checkbox-track.gray::after { border-color: ${Colors.gray}; } .checkbox-track.dark-gray::after { border-color: ${Colors.darkGray}; } .checkbox-track.charcoal::after { border-color: ${Colors.charcoal}; } .checkbox-track.magenta::after { border-color: ${Colors.magenta}; } .checkbox-track.red::after { border-color: ${Colors.red}; } .checkbox-track.red-orange::after { border-color: ${Colors.redOrange}; } .checkbox-track.orange::after { border-color: ${Colors.orange}; } .checkbox-track.gold::after { border-color: ${Colors.gold}; } .checkbox-track.yellow::after { border-color: ${Colors.yellow}; } .checkbox-track.grass-green::after { border-color: ${Colors.grassGreen}; } .checkbox-track.green::after { border-color: ${Colors.green}; } .checkbox-track.dark-green::after { border-color: ${Colors.darkGreen}; } .checkbox-track.teal::after { border-color: ${Colors.teal}; } .checkbox-track.blue::after { border-color: ${Colors.blue}; } .checkbox-track.indigo::after { border-color: ${Colors.indigo}; } .checkbox-track.purple::after { border-color: ${Colors.purple}; } .checkbox-track.violet::after { border-color: ${Colors.violet}; } .checkbox-track.beige::after { border-color: ${Colors.beige}; } .checkbox-track.light-brown::after { border-color: ${Colors.lightBrown}; } .checkbox-track.brown::after { border-color: ${Colors.brown}; } .checkbox-track.dark-brown::after { border-color: ${Colors.darkBrown}; } .checkbox-track.pastel-pink::after { border-color: ${Colors.pastelPink}; } .checkbox-track.pastel-orange::after { border-color: ${Colors.pastelOrange}; } .checkbox-track.pastel-yellow::after { border-color: ${Colors.pastelYellow}; } .checkbox-track.pastel-green::after { border-color: ${Colors.pastelGreen}; } .checkbox-track.pastel-blue::after { border-color: ${Colors.pastelBlue}; } .checkbox-track.pastel-purple::after { border-color: ${Colors.pastelPurple}; } .checkbox-track.light-blue::after { border-color: ${Colors.lightBlue}; } .checkbox-track.pink::after { border-color: ${Colors.pink}; } input:checked + .checkbox-track { background-color: ${Colors.darkGreen}; } input:checked + .checkbox-track.black { background-color: ${Colors.black}; border-color: ${Colors.black}; color: ${Colors.white}; } input:checked + .checkbox-track.white { background-color: ${Colors.white}; color: ${Colors.black}; } input:checked + .checkbox-track.silver { background-color: ${Colors.silver}; color: ${Colors.black}; } input:checked + .checkbox-track.gray { background-color: ${Colors.gray}; color: ${Colors.white}; } input:checked + .checkbox-track.dark-gray { background-color: ${Colors.darkGray}; color: ${Colors.white}; } input:checked + .checkbox-track.charcoal { background-color: ${Colors.charcoal}; color: ${Colors.white}; } input:checked + .checkbox-track.magenta { background-color: ${Colors.magenta}; color: ${Colors.white}; } input:checked + .checkbox-track.red { background-color: ${Colors.red}; color: ${Colors.white}; } input:checked + .checkbox-track.red-orange { background-color: ${Colors.redOrange}; color: ${Colors.white}; } input:checked + .checkbox-track.orange { background-color: ${Colors.orange}; color: ${Colors.black}; } input:checked + .checkbox-track.gold { background-color: ${Colors.gold}; color: ${Colors.black}; } input:checked + .checkbox-track.yellow { background-color: ${Colors.yellow}; color: ${Colors.black}; } input:checked + .checkbox-track.grass-green { background-color: ${Colors.grassGreen}; color: ${Colors.black}; } input:checked + .checkbox-track.green { background-color: ${Colors.green}; color: ${Colors.black}; } input:checked + .checkbox-track.dark-green { background-color: ${Colors.darkGreen}; color: ${Colors.white}; } input:checked + .checkbox-track.teal { background-color: ${Colors.teal}; color: ${Colors.white}; } input:checked + .checkbox-track.blue { background-color: ${Colors.blue}; color: ${Colors.white}; } input:checked + .checkbox-track.indigo { background-color: ${Colors.indigo}; color: ${Colors.white}; } input:checked + .checkbox-track.purple { background-color: ${Colors.purple}; color: ${Colors.white}; } input:checked + .checkbox-track.violet { background-color: ${Colors.violet}; color: ${Colors.white}; } input:checked + .checkbox-track.beige { background-color: ${Colors.beige}; color: ${Colors.black}; } input:checked + .checkbox-track.light-brown { background-color: ${Colors.lightBrown}; color: ${Colors.white}; } input:checked + .checkbox-track.brown { background-color: ${Colors.brown}; color: ${Colors.white}; } input:checked + .checkbox-track.dark-brown { background-color: ${Colors.darkBrown}; color: ${Colors.white}; } input:checked + .checkbox-track.pastel-pink { background-color: ${Colors.pastelPink}; color: ${Colors.black}; } input:checked + .checkbox-track.pastel-orange { background-color: ${Colors.pastelOrange}; color: ${Colors.black}; } input:checked + .checkbox-track.pastel-yellow { background-color: ${Colors.pastelYellow}; color: ${Colors.black}; } input:checked + .checkbox-track.pastel-green { background-color: ${Colors.pastelGreen}; color: ${Colors.black}; } input:checked + .checkbox-track.pastel-blue { background-color: ${Colors.pastelBlue}; color: ${Colors.black}; } input:checked + .checkbox-track.pastel-purple { background-color: ${Colors.pastelPurple}; color: ${Colors.black}; } input:checked + .checkbox-track.light-blue { background-color: ${Colors.lightBlue}; color: ${Colors.black}; } input:checked + .checkbox-track.pink { background-color: ${Colors.pink}; color: ${Colors.white}; } input:checked + .checkbox-track:before { top: 5px; } input:checked + .checkbox-track:after { transform: translateX(35px); } .on-text { position: absolute; top: 9px; left: 12px; font-size: 12px; display: none; } .on-text.show { display: inline; } .off-text { position: absolute; top: 9px; left: 30px; font-size: 12px; display: none; } .off-text.show { display: inline; } input[type="range"] { margin: auto; } input[type="range"]:focus:not(:focus-visible) { border: none; } input[type="range"]:focus-visible { box-shadow: 0px 0px 0px 2px currentColor; } .slider-container { width: 100%; padding-bottom: 8px; } .slider { -webkit-appearance: none; appearance: none; width: 100%; height: 2px; margin-bottom: 10px; background-color: gray; outline: none; opacity: 0.7; -webkit-transition: .2s; transition: opacity .2s; display: none; } .slider.show { display: inline-block; } .slider:hover { opacity: 1; } /* prevent Firefox from adding extra styling on focused slider */ input[type=range]::-moz-focus-outer { border: 0; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 10px; height: 25px; border-radius: 5px; border: none; cursor: pointer; } input[type="range"]:focus:not(:focus-visible)::-webkit-slider-thumb { border: none; } input[type="range"]:focus-visible::-webkit-slider-thumb { border: 2px solid currentColor; } input[type="range"]::-moz-range-thumb { width: 10px; height: 25px; border: none; cursor: pointer; } .slider.black::-webkit-slider-thumb { background-color: ${Colors.black}; } .slider.black::-moz-range-thumb { background-color: ${Colors.black}; } .slider.white::-webkit-slider-thumb { background-color: ${Colors.white}; } .slider.white::-moz-range-thumb { background-color: ${Colors.white}; } .slider.silver::-webkit-slider-thumb { background-color: ${Colors.silver}; } .slider.silver::-moz-range-thumb { background-color: ${Colors.silver}; } .slider.gray::-webkit-slider-thumb { background-color: ${Colors.gray}; } .slider.gray::-moz-range-thumb { background-color: ${Colors.gray}; } .slider.dark-gray::-webkit-slider-thumb { background-color: ${Colors.darkGray}; } .slider.dark-gray::-moz-range-thumb { background-color: ${Colors.darkGray}; } .slider.charcoal::-webkit-slider-thumb { background-color: ${Colors.charcoal}; } .slider.charcoal::-moz-range-thumb { background-color: ${Colors.charcoal}; } .slider.magenta::-webkit-slider-thumb { background-color: ${Colors.magenta}; } .slider.magenta::-moz-range-thumb { background-color: ${Colors.magenta}; } .slider.red::-webkit-slider-thumb { background-color: ${Colors.red}; } .slider.red::-moz-range-thumb { background-color: ${Colors.red}; } .slider.red-orange::-webkit-slider-thumb { background-color: ${Colors.redOrange}; } .slider.red-orange::-moz-range-thumb { background-color: ${Colors.redOrange}; } .slider.orange::-webkit-slider-thumb { background-color: ${Colors.orange}; } .slider.orange::-moz-range-thumb { background-color: ${Colors.orange}; } .slider.gold::-webkit-slider-thumb { background-color: ${Colors.gold}; } .slider.gold::-moz-range-thumb { background-color: ${Colors.gold}; } .slider.yellow::-webkit-slider-thumb { background-color: ${Colors.yellow}; } .slider.yellow::-moz-range-thumb { background-color: ${Colors.yellow}; } .slider.grass-green::-webkit-slider-thumb { background-color: ${Colors.grassGreen}; } .slider.grass-green::-moz-range-thumb { background-color: ${Colors.grassGreen}; } .slider.green::-webkit-slider-thumb { background-color: ${Colors.green}; } .slider.green::-moz-range-thumb { background-color: ${Colors.green}; } .slider.dark-green::-webkit-slider-thumb { background-color: ${Colors.darkGreen}; } .slider.dark-green::-moz-range-thumb { background-color: ${Colors.darkGreen}; } .slider.teal::-webkit-slider-thumb { background-color: ${Colors.teal}; } .slider.teal::-moz-range-thumb { background-color: ${Colors.teal}; } .slider.blue::-webkit-slider-thumb { background-color: ${Colors.blue}; } .slider.blue::-moz-range-thumb { background-color: ${Colors.blue}; } .slider.indigo::-webkit-slider-thumb { background-color: ${Colors.indigo}; } .slider.indigo::-moz-range-thumb { background-color: ${Colors.indigo}; } .slider.violet::-webkit-slider-thumb { background-color: ${Colors.violet}; } .slider.violet::-moz-range-thumb { background-color: ${Colors.violet}; } .slider.purple::-webkit-slider-thumb { background-color: ${Colors.purple}; } .slider.purple::-moz-range-thumb { background-color: ${Colors.purple}; } .slider.beige::-webkit-slider-thumb { background-color: ${Colors.beige}; } .slider.beige::-moz-range-thumb { background-color: ${Colors.beige}; } .slider.light-brown::-webkit-slider-thumb { background-color: ${Colors.lightBrown}; } .slider.light-brown::-moz-range-thumb { background-color: ${Colors.lightBrown}; } .slider.brown::-webkit-slider-thumb { background-color: ${Colors.brown}; } .slider.brown::-moz-range-thumb { background-color: ${Colors.brown}; } .slider.dark-brown::-webkit-slider-thumb { background-color: ${Colors.darkBrown}; } .slider.dark-brown::-moz-range-thumb { background-color: ${Colors.darkBrown}; } .slider.pastel-pink::-webkit-slider-thumb { background-color: ${Colors.pastelPink}; } .slider.pastel-pink::-moz-range-thumb { background-color: ${Colors.pastelPink}; } .slider.pastel-orange::-webkit-slider-thumb { background-color: ${Colors.pastelOrange}; } .slider.pastel-orange::-moz-range-thumb { background-color: ${Colors.pastelOrange}; } .slider.pastel-yellow::-webkit-slider-thumb { background-color: ${Colors.pastelYellow}; } .slider.pastel-yellow::-moz-range-thumb { background-color: ${Colors.pastelYellow}; } .slider.pastel-green::-webkit-slider-thumb { background-color: ${Colors.pastelGreen}; } .slider.pastel-green::-moz-range-thumb { background-color: ${Colors.pastelGreen}; } .slider.pastel-blue::-webkit-slider-thumb { background-color: ${Colors.pastelBlue}; } .slider.pastel-blue::-moz-range-thumb { background-color: ${Colors.pastelBlue}; } .slider.pastel-purple::-webkit-slider-thumb { background-color: ${Colors.pastelPurple}; } .slider.pastel-purple::-moz-range-thumb { background-color: ${Colors.pastelPurple}; } .slider.light-blue::-webkit-slider-thumb { background-color: ${Colors.lightBlue}; } .slider.light-blue::-moz-range-thumb { background-color: ${Colors.lightBlue}; } .slider.pink::-webkit-slider-thumb { background-color: ${Colors.pink}; } .slider.pink::-moz-range-thumb { background-color: ${Colors.pink}; } .more-options-dropdown { display: none; padding: 10px; font-family: sans-serif; font-size: 16px; } .more-options-dropdown.show { display: block; } #snackbar { visibility: hidden; min-width: 250px; background-color: ${Colors.colorPaletteBackground}; color: ${Colors.black}; font-size: 16px; font-family: sans-serif; text-align: center; border-radius: 2px; padding: 16px; position: fixed; z-index: 1; right: 50%; transform: translateX(50%); bottom: 30px; } #snackbar.show { visibility: visible; -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; animation: fadein 0.5s, fadeout 0.5s 2.5s; } @-webkit-keyframes fadein { from {bottom: 0; opacity: 0;} to {bottom: 30px; opacity: 1;} } @keyframes fadein { from {bottom: 0; opacity: 0;} to {bottom: 30px; opacity: 1;} } @-webkit-keyframes fadeout { from {bottom: 30px; opacity: 1;} to {bottom: 0; opacity: 0;} } @keyframes fadeout { from {bottom: 30px; opacity: 1;} to {bottom: 0; opacity: 0;} } `, ] } }