Got the roundtrip working. But the endstate math is off.

This commit is contained in:
Elf M. Sternberg 2025-01-20 13:28:22 -08:00
parent 764523ed4f
commit f521b261e6
5 changed files with 312 additions and 49 deletions

22
src/events.ts Normal file
View File

@ -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;
}
}

212
src/fridge-board.ts Normal file
View File

@ -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`<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;
};
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;
}
}

View File

@ -1,7 +1,9 @@
import { LitElement, html, css } from "lit"; import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators/custom-element.js"; import { customElement } from "lit/decorators/custom-element.js";
import { words } from "./wordlist.js"; import { words } from "./wordlist.js";
import "./fridge-tile.js"; import { PointerLocationRequest } from "./events.js";
import "./fridge-board.js";
@customElement("fridge-magnets") @customElement("fridge-magnets")
export class FridgeMagnets extends LitElement { export class FridgeMagnets extends LitElement {
@ -10,34 +12,51 @@ export class FridgeMagnets extends LitElement {
:host { :host {
display: block; display: block;
} }
#fridgemagnets {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: 100fr 18ex;
}
#fridge { fridge-board {
overflow: hidden; display: block;
position: relative; position: relative;
width: 100%; width: 100%;
height: 95vw;
background: url("./dist/pingbg.png") repeat; 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() { render() {
return html` <div id="fridgemagnets"> return html`<div id="body">
<div id="fridge"> <fridge-board .words=${this.words} id="fridge"></fridge-broad>
${words.map(word => html`<fridge-tile word=${word.w}></fridge-tile>`)} <p slot="footer">Some Content will go here.</p>
</div>
<div id="footer">Footer</div>
</div>`; </div>`;
} }
} }

View File

@ -49,48 +49,53 @@ export class FridgeTile extends LitElement {
position: relative; position: relative;
background: white; background: white;
z-index: 100; 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() { get position() {
super(); const { left, top } = this.getBoundingClientRect();
this.dragHandle = new LitDraggable(this); return [left, top];
this.onDragEnd = this.onDragEnd.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.addEventListener(LitDragEnd.eventName, this.onDragEnd);
this.addEventListener(LitDragStart.eventName, this.onDragStart);
} }
onDragEnd(ev: LitDragEvent) { get relativePosition() {
this.transform = { scale: 1.0, rotate: Math.random() * 30 - 15 }; return [this.offsetLeft, this.offsetTop];
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`);
} }
onDragStart(_ev: LitDragEvent) { get size() {
this.transform = { scale: 1.3, rotate: Math.random() * 30 - 15 }; const { width, height } = this.getBoundingClientRect();
this.handle.value!.style.setProperty( return { width, height };
"transform", }
`scale(${+this.transform.scale}) rotate(${+this.transform.rotate}deg)`
); 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() { render() {
const styles = { const styles = {
width: `${this.word.length * 1.2}ch`, width: `${this.word.length * 1.2}ch`,
transform: `rotate(${this.transform.rotate}deg)`,
}; };
return html`<div part="word ${ref(this.handle)} " style="${styleMap(styles)}" class="word">${this.word}</div>`; return html`<div part="word ${ref(this.handle)} " style="${styleMap(styles)}" class="word">${this.word}</div>`;
} }

View File

@ -11,3 +11,8 @@ export interface LitDragEvent extends Event {
offsetY: number; offsetY: number;
node: HTMLElement; node: HTMLElement;
} }
export interface Position {
x: number;
y: number;
}