diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..6d629ab --- /dev/null +++ b/src/events.ts @@ -0,0 +1,22 @@ +import type { Position } from "./types"; + +export class PointerLocationRequest extends Event { + static readonly eventName = "fridge-pointer-location"; + position!: Position; + constructor() { + super(PointerLocationRequest.eventName, { bubbles: true, composed: true }); + } +} + +export function pointerLocation(self?: HTMLElement): Position { + const node = self ?? window; + const ev = new PointerLocationRequest(); + node.dispatchEvent(ev); + return ev.position; +} + +declare global { + interface GlobalEventHandlersEventMap { + [PointerLocationRequest.eventName]: PointerLocationRequest; + } +} diff --git a/src/fridge-board.ts b/src/fridge-board.ts new file mode 100644 index 0000000..9f962a4 --- /dev/null +++ b/src/fridge-board.ts @@ -0,0 +1,212 @@ +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; + } + + .ready-to-slide { + transition: transform 1500ms cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform; + } + `; + } + + @property({ type: Array }) + words: (typeof words)[] = []; + + @queryAll("fridge-tile") + tiles!: FridgeTile[]; + + @query("#fridge") + fridge!: HTMLDivElement; + + 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 newXPos = Math.random() * (boardHeight - tileHeight) * 0.985; + const newYPos = Math.random() * (boardWidth - tileWidth) * 0.98; + + // 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`; + 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 with respect to the viewport; + const { left: fridgeLeft, top: fridgeTop } = this.getBoundingClientRect(); + + // The position of the node with respect to the viewport: + const { left: nodeLeft, top: nodeTop } = node.getBoundingClientRect(); + + // Where the pointer was when the event started with respect to the viewport; + const { x: pointerStartX, y: pointerStartY } = pointerLocation(); + + // 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 stop = () => { + console.log("Stop called?"); + controller.abort(); + tracking = false; + const cursorPosition = pointerLocation(); + let newX = tileStart.x - (cursorPosition.x - pointerStartX); + let newY = tileStart.y - (cursorPosition.y - pointerStartY); + node.style.setProperty("transform", ""); + node.style.setProperty("top", `${newY}px`); + node.style.setProperty("left", `${newX}px`); + }; + + let animationFrame: number = -1; + + const move = () => { + const cursorPosition = pointerLocation(); + let delX = cursorPosition.x - pointerStartX; + let delY = cursorPosition.y - pointerStartY; + node.style.setProperty("transform", `translate3d(${+delX}px, ${+delY}px, 0)`); + + if (tracking) { + animationFrame = requestAnimationFrame(move); + } else { + cancelAnimationFrame(animationFrame); + } + }; + + 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; + }; + + requestAnimationFrame(() => + this.tiles.forEach((tile) => { + const outerX = fd(boardWidth); + const outerY = fd(boardHeight); + const [tileX, tileY] = tile.relativePosition; + tile.style.transform = `translate(${outerX - tileX}px, ${outerY - tileY}px) rotate(${Math.random() * 30 - 15}deg) scale(1.5)`; + tile.style.opacity = "100%"; + }) + ); + + requestAnimationFrame(() => + this.tiles.forEach((tile) => { + tile.classList.add("ready-to-slide"); + tile.style.transform = `translate(0, 0) rotate(${Math.random() * 30}deg) scale(1.0)`; + }) + ); + + setTimeout( + () => + this.tiles.forEach((tile) => { + tile.classList.remove("ready-to-slide"); + }), + 1525 + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "fridge-board": FridgeBoard; + } +} diff --git a/src/fridge-magnets.ts b/src/fridge-magnets.ts index 9a046dd..2f11f13 100644 --- a/src/fridge-magnets.ts +++ b/src/fridge-magnets.ts @@ -1,7 +1,9 @@ import { LitElement, html, css } from "lit"; import { customElement } from "lit/decorators/custom-element.js"; import { words } from "./wordlist.js"; -import "./fridge-tile.js"; +import { PointerLocationRequest } from "./events.js"; + +import "./fridge-board.js"; @customElement("fridge-magnets") export class FridgeMagnets extends LitElement { @@ -10,34 +12,51 @@ export class FridgeMagnets extends LitElement { :host { display: block; } - #fridgemagnets { - width: 100%; - height: 100%; - display: grid; - grid-template-rows: 100fr 18ex; - } - #fridge { - overflow: hidden; + fridge-board { + display: block; position: relative; width: 100%; + height: 95vw; background: url("./dist/pingbg.png") repeat; } - - #footer { - background-color: #32cd32; - width: 100%; - height: 18ex; - } `; } + words = words; + + pointerLocation: { x: number; y: number } = { x: -1, y: -1 }; + + constructor() { + super(); + this.updatePointerPosition = this.updatePointerPosition.bind(this); + this.onPointerLocation = this.onPointerLocation.bind(this); + } + + updatePointerPosition(ev: PointerEvent) { + this.pointerLocation = { x: ev.clientX, y: ev.clientY }; + } + + onPointerLocation(ev: PointerLocationRequest) { + ev.position = this.pointerLocation; + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener("pointermove", this.updatePointerPosition); + window.addEventListener(PointerLocationRequest.eventName, this.onPointerLocation); + } + + disconnectedCallback() { + window.removeEventListener("pointermove", this.updatePointerPosition); + window.removeEventListener(PointerLocationRequest.eventName, this.onPointerLocation); + super.disconnectedCallback(); + } + render() { - return html`
-
- ${words.map(word => html``)} -
- + return html`
+ +

Some Content will go here.

`; } } diff --git a/src/fridge-tile.ts b/src/fridge-tile.ts index 124ecbc..ad4f77a 100644 --- a/src/fridge-tile.ts +++ b/src/fridge-tile.ts @@ -49,48 +49,53 @@ export class FridgeTile extends LitElement { position: relative; background: white; z-index: 100; - transition-property: transform; - transition-duration: 200ms; - transition-timing-function: ease-in-out; //other options are ease - } - - .word.dragging { - font-size: 1.1875rem; } `; } - constructor() { - super(); - this.dragHandle = new LitDraggable(this); - this.onDragEnd = this.onDragEnd.bind(this); - this.onDragStart = this.onDragStart.bind(this); - this.addEventListener(LitDragEnd.eventName, this.onDragEnd); - this.addEventListener(LitDragStart.eventName, this.onDragStart); + get position() { + const { left, top } = this.getBoundingClientRect(); + return [left, top]; } - onDragEnd(ev: LitDragEvent) { - this.transform = { scale: 1.0, rotate: Math.random() * 30 - 15 }; - this.handle.value!.style.setProperty( - "transform", - `scale(${+this.transform.scale}) rotate(${+this.transform.rotate}deg)` - ); - this.style.setProperty("top", `${+ev.offsetY}px`); - this.style.setProperty("left", `${+ev.offsetX}px`); + get relativePosition() { + return [this.offsetLeft, this.offsetTop]; } - onDragStart(_ev: LitDragEvent) { - this.transform = { scale: 1.3, rotate: Math.random() * 30 - 15 }; - this.handle.value!.style.setProperty( - "transform", - `scale(${+this.transform.scale}) rotate(${+this.transform.rotate}deg)` - ); + get size() { + const { width, height } = this.getBoundingClientRect(); + return { width, height }; + } + + get tl() { + return this.relativePosition; + } + + get tr() { + const [x, y] = this.relativePosition; + const { width } = this.size; + return [x + width, y]; + } + + get bl() { + const [x, y] = this.relativePosition; + const { height } = this.size; + return [x + height, y]; + } + + get br() { + const [x, y] = this.relativePosition; + const { width, height } = this.size; + return [x + width, y + height]; + } + + get points() { + return [this.tl, this.tr, this.bl, this.br]; } render() { const styles = { width: `${this.word.length * 1.2}ch`, - transform: `rotate(${this.transform.rotate}deg)`, }; return html`
${this.word}
`; } diff --git a/src/types.ts b/src/types.ts index a053696..b28ac3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,3 +11,8 @@ export interface LitDragEvent extends Event { offsetY: number; node: HTMLElement; } + +export interface Position { + x: number; + y: number; +}