fridgemagnets/src/fridge-board.ts

252 lines
8.4 KiB
TypeScript

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`<div id="fridge" @pointerdown=${this.onPointerDown}>
${words.map(word => html`<fridge-tile style="opacity: 0" word=${word.w}></fridge-tile>`)}
</div>`;
}
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;
}
}