import { LitElement, html, css } from "lit"; import { customElement } from "lit/decorators/custom-element.js"; import { queryAll } from "lit/decorators/query-all.js"; import { query } from "lit/decorators/query.js"; import { words } from "./wordlist.js"; import "./fridge-tile.js"; import { property } from "lit/decorators/property.js"; import { FridgeTile } from "./fridge-tile.js"; import { pointerLocation } from "./events.js"; function* pointsIterator(source: FridgeTile[]) { for (const tile of source) { for (const point of tile.points) { yield point; } } } type Point = [number, number]; const isHTMLElement = (v: unknown): v is HTMLElement => typeof v === "object" && v !== null && "nodeType" in v && v.nodeType === Node.ELEMENT_NODE; @customElement("fridge-board") export class FridgeBoard extends LitElement { static get styles() { return css` :host { display: block; position: relative; } #fridge { overflow: hidden; position: relative; top: 0; right: 0; width: 100%; height: 100%; background: transparent; } fridge-tile { position: absolute; transition: scale 200ms ease-out, rotate 200ms ease-out; will-change: scale rotate translate; } .ready-to-slide { transition: scale 1500ms ease-out, rotate 1500ms ease-out, translate 1500ms cubic-bezier(0.4, 0, 0.2, 1); will-change: scale rotate translate; } `; } @property({ type: Array }) words: (typeof words)[] = []; @queryAll("fridge-tile") tiles!: FridgeTile[]; @query("#fridge") fridge!: HTMLDivElement; maxZ: number = 1; assignNewPositions() { const pointsAlreadyPositioned: Point[] = []; const unpositionable: FridgeTile[] = []; const { width: boardWidth, height: boardHeight } = this.getBoundingClientRect(); const setNewPosition = (tile: FridgeTile) => { const { height: tileHeight, width: tileWidth } = tile.size; for (let i = 0; i < 30; i++) { // Where we will put the new tile const newYPos = Math.random() * (boardHeight - tileHeight) * 0.96; const newXPos = Math.random() * (boardWidth - tileWidth) * 0.96; // The box defining the new tile. const box = { xl: newXPos, xr: newXPos + tileWidth, yt: newYPos, yb: newYPos + tileHeight, }; const isPointInBox = (point: Point) => point[0] >= box.xl && point[0] <= box.xr && point[1] >= box.yt && point[1] <= box.yb; if (pointsAlreadyPositioned.find(p => isPointInBox(p)) === undefined) { tile.style.top = `${box.yt}px`; tile.style.left = `${box.xl}px`; pointsAlreadyPositioned.push( [box.xl, box.yt], [box.xr, box.yt], [box.xl, box.yb], [box.xr, box.yb] ); return true; } } return false; }; for (const tile of this.tiles) { if (!setNewPosition(tile)) { unpositionable.push(tile); } } for (const tile of unpositionable) { tile.remove(); } } onPointerDown(ev: PointerEvent) { const node = ev.target; if (!(isHTMLElement(node) && node.tagName.toLowerCase() === "fridge-tile")) { return; } // The position of the board relative to the viewport; const { left: fridgeLeft, top: fridgeTop } = this.getBoundingClientRect(); // The position of the node relative to the viewport: const { left: nodeLeft, top: nodeTop } = node.getBoundingClientRect(); // The position of the pointer when the event started relative to the viewport; const { clientX: pointerStartX, clientY: pointerStartY } = ev; let pointerX = pointerStartX; let pointerY = pointerStartY; // Starting position of the *node* with respect to the board; const tileStart = { x: nodeLeft - fridgeLeft, y: nodeTop - fridgeTop, }; const controller = new AbortController(); const { signal } = controller; let tracking = true; const pointer = (ev: PointerEvent) => { const { clientX, clientY } = ev; pointerX = clientX; pointerY = clientY; }; this.maxZ = this.maxZ + 1; node.style.setProperty("z-index", `${this.maxZ}`); node.style.setProperty("scale", "1.3"); node.style.setProperty("rotate", `${Math.random() * 30 - 15}deg`); const stop = () => { tracking = false; controller.abort(); let delX = pointerX - pointerStartX; let delY = pointerY - pointerStartY; node.style.removeProperty("translate"); node.style.setProperty("top", `${tileStart.y + delY}px`); node.style.setProperty("left", `${tileStart.x + delX}px`); requestAnimationFrame(() => { node.style.setProperty("scale", "1.0"); node.style.setProperty("rotate", `${Math.random() * 30 - 15}deg`); }); }; let animationFrame: number = -1; const move = () => { if (tracking) { let delX = pointerX - pointerStartX; let delY = pointerY - pointerStartY; node.style.setProperty("translate", `${+delX}px ${+delY}px 0`); animationFrame = requestAnimationFrame(move); return; } cancelAnimationFrame(animationFrame); return; }; window.addEventListener("pointermove", pointer, { signal }); window.addEventListener("pointerup", stop, { signal }); window.addEventListener("pointercancel", stop, { signal }); animationFrame = requestAnimationFrame(move); } render() { return html`
${words.map(word => html``)}
`; } updated() { const { width: boardWidth, height: boardHeight } = this.getBoundingClientRect(); this.assignNewPositions(); const fd = (size: number) => { const newDelta = 40 * Math.random(); return Math.random() < 0.5 ? newDelta + size : newDelta * -1; }; const lastTile = this.tiles[this.tiles.length - 1]; const slideEnd = (ev: TransitionEvent) => { if (ev.propertyName === "translate") { this.tiles.forEach(tile => { tile.classList.remove("ready-to-slide"); }); lastTile.removeEventListener("transitionend", slideEnd); } }; requestAnimationFrame(() => { this.tiles.forEach(tile => { const outerX = fd(boardWidth); const outerY = fd(boardHeight); const [tileX, tileY] = tile.relativePosition; tile.style.setProperty("translate", `${outerX - tileX}px ${outerY - tileY}px 0`); tile.style.setProperty("scale", "1.5"); tile.style.setProperty("rotate", `${Math.random() * 720}deg`); tile.style.setProperty("opacity", "100%"); }); lastTile.addEventListener("transitionend", slideEnd); requestAnimationFrame(() => this.tiles.forEach(tile => { tile.classList.add("ready-to-slide"); tile.style.setProperty("translate", "0 0 0"); tile.style.setProperty("rotate", `${Math.random() * 30 - 15}deg`); tile.style.setProperty("scale", "1.0"); }) ); }); } } declare global { interface HTMLElementTagNameMap { "fridge-board": FridgeBoard; } }