From 02f8f278395d467669fd7665177c91084a9013c5 Mon Sep 17 00:00:00 2001 From: a Date: Mon, 14 Sep 2020 15:24:32 +0200 Subject: [PATCH] Animated carousel --- src/index.ts | 135 ++++++++++++++++++++++++++++++++++++++++----------- src/utils.ts | 41 +++++++++++++++- 2 files changed, 145 insertions(+), 31 deletions(-) diff --git a/src/index.ts b/src/index.ts index ad14da9..95be104 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ -import { css, html, LitElement } from "lit-element"; -import { cyclicArray } from "./utils"; +import { animationPromise, cyclicArray, Alarms, html } from "./utils"; const items = [ { @@ -23,39 +22,125 @@ const items = [ }, ]; -class Carousel extends LitElement { - currentIndex = 0; - currentItem = items[this.currentIndex]; +class Carousel extends HTMLElement { + items = cyclicArray(items); + shadow; constructor() { super(); + this.shadow = this.attachShadow({ mode: "open" }); } - render() { - return html` - - - - + connectedCallback() { + this.shadow.innerHTML = html` + +
+ +
+ + `; + + const next = this.shadow.querySelector(".next") as HTMLButtonElement; + const prev = this.shadow.querySelector(".prev") as HTMLButtonElement; + next.addEventListener("click", () => this.rotate()); + prev.addEventListener("click", () => this.rotate(true)); + Alarms.create(6000, "rotateAlarm").then(() => this.rotate()); } - static get styles() { - return css` + rotate(reverse = false) { + Alarms.create(6000, "rotateAlarm").then(() => this.rotate()); + + const container = this.shadow.querySelector(".container") as HTMLDivElement; + const oldItem = container.querySelector( + "img:last-of-type" + ) as HTMLImageElement; //In case we are waiting for animation to finish + const newItem = document.createElement("img") as HTMLImageElement; + + const inClass = reverse ? ["reverse", "left"] : ["right"]; + const outClass = reverse ? ["reverse", "right"] : ["left"]; + + newItem.src = this.items.next(reverse).value.img; + container.appendChild(newItem); + + animationPromise(oldItem).finally(() => { + container.removeChild(oldItem); + }); + oldItem.classList.add(...outClass); + + animationPromise(newItem).finally(() => + newItem.classList.remove(...inClass) + ); + newItem.classList.add(...inClass); + } + + static get css() { + return ` :host { - max-width: 100%; + width: 100%; height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + + .container { height: 100%; + width: 60%; + clip-path: margin-box; + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; } img { - max-height: 100%; - box-shadow: 0px 0px 5px 0px var(--background-opposite-1); - transition: 0.2s; + animation-duration: 1.5s; + object-fit: scale-down; + width: 100%; + height: 100%; } - img:hover, button:hover~img { - transform: scale(1.05); + @keyframes slide-to-left { + from { + transform: translateX(0%); + } + to { + transform: translateX(-200%); + } + } + + @keyframes slide-from-right { + from { + transform: translateX(200%); + } + to { + transform: translateX(0%); + } + } + + .right { + animation-name: slide-from-right; + } + + .left { + animation-name: slide-to-left; + position: absolute; + } + + .reverse { + animation-direction: reverse; + } + + .left.reverse { + position: static; + } + + .right.reverse { + position: absolute; } button { @@ -63,18 +148,10 @@ class Carousel extends LitElement { border-radius: 50%; background-color: var(--background-primary-2); cursor: pointer; - position: relative; - top:-50%; z-index: 1; + color: var(--text-color-3); + margin: -20%; } - - .left { - left: 20%; - } - .right { - left: 80%; - } - `; } } diff --git a/src/utils.ts b/src/utils.ts index cd9b3ab..3d7a2f4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,12 @@ -/// A generator that infinitely loops around an array -export function* cyclicArray(array: T[]) { +/** + A generator that infinitely loops around an array + + @param array The array to loop around + @yields Yields items from the array wrapping around to the start. + @next Decideds if the iterator should go backwards + */ + +export function* cyclicArray(array: T[]): Generator { let index = 0; while (true) { const reverse = yield array[index]; @@ -11,12 +18,23 @@ export function* cyclicArray(array: T[]) { } } +/** + Rotates an array + + @param arr The array to rotate + @param reverse Should the array be rotated in the opposite direction + @returns The rotated array. For example by passing in [1,2,3] you will recieve [3,1,2] +*/ export const rotateArray = (arr: T[], reverse = false): T[] => { if (reverse) arr.unshift(arr.pop()!); else arr.push(arr.shift()!); return arr; }; +/** + Ergonomic wrapper for creating new promises + @returns A tuple of the created promise and it's resolve and reject functions +*/ type resolveCallback = (value: T) => void; type rejectCallback = (reason?: unknown) => void; export const newPromise = (): [ @@ -60,3 +78,22 @@ export namespace Alarms { }; export const clearAll = () => alarmsMap.forEach((a) => a[1]()); } + +/** + @returns Promise that resolves when an animation on the element finishes +*/ +export const animationPromise = (element: HTMLElement) => { + const [promise, reject, resolve] = newPromise(); + element.addEventListener("animationend", () => resolve(), true); + element.addEventListener("animationcancel", () => reject(), true); + return promise; +}; + +/** + Escapes all the keys in the template string +*/ +export const html = (strings: TemplateStringsArray, ...keys: string[]) => + strings.reduce((p, c, i) => p + escape(keys[i - 1]) + c); + +export const escape = (string: string) => + string.replace(//g, ">");