yummy.cricket/run.ts

303 lines
8.7 KiB
TypeScript

/** one of the six document sections */
export type Pane = 'hello' | 'id' | 'activities' | 'links' | 'friends' | 'six';
export const allPanes: Pane[] =
['hello', 'id', 'activities', 'links', 'friends', 'six'];
namespace Cube {
/** location on the cube in space */
export type Face = 'front' | 'top' | 'back' | 'bottom' | 'left' | 'right';
/**
* - for front, left, right: up is up
* - for back: up is down (lol)
* - for top: up is away from you
* - for bottom: up is towards you
*/
export type Orientation = 'up' | 'left' | 'down' | 'right';
export type Place = [Face, Orientation];
function table<A extends string, B = A>(m: Record<A, B>): (x: A) => B {
return x => m[x];
}
const doCwO =
table<Orientation>({up: 'right', right: 'down', down: 'left', left: 'up'});
const doCcwO =
table<Orientation>({up: 'left', left: 'down', down: 'right', right: 'up'});
export type RotateXY = 'up' | 'left' | 'down' | 'right';
export type RotateZ = 'cw' | 'ccw';
export type Rotation = RotateXY | RotateZ;
// if you rotate the cube "up" (along the x axis), face f becomes up(f)
// et cetera
const moveF: Record<Rotation, (f: Face) => Face> = {
up: table({
front: 'top', top: 'back', back: 'bottom',
bottom: 'front', left: 'left', right: 'right'
}),
down: table({
front: 'bottom', top: 'front', back: 'top',
bottom: 'back', left: 'left', right: 'right'
}),
left: table({
front: 'left', top: 'top', back: 'right',
bottom: 'bottom', left: 'back', right: 'front'
}),
right: table({
front: 'right', top: 'top', back: 'left',
bottom: 'bottom', left: 'front', right: 'back'
}),
cw: table({
front: 'front', back: 'back', left: 'top',
right: 'bottom', top: 'right', bottom: 'left'
}),
ccw: table({
front: 'front', back: 'back', left: 'bottom',
right: 'top', top: 'left', bottom: 'right'
})
};
const moveO: Record<Rotation, (f: Face, o: Orientation) => Orientation> = {
up(f, o) {
switch (f) {
case 'left': return doCcwO(o);
case 'right': return doCwO(o);
default: return o;
}
},
down(f, o) {
switch (f) {
case 'left': return doCwO(o);
case 'right': return doCcwO(o);
default: return o;
}
},
left(f, o) {
switch (f) {
case 'top': return doCwO(o);
case 'bottom': return doCcwO(o);
case 'left':
case 'back': return doCwO(doCwO(o));
default: return o;
}
},
right(f, o) {
switch (f) {
case 'top': return doCcwO(o);
case 'bottom': return doCwO(o);
case 'right':
case 'back': return doCcwO(doCcwO(o));
default: return o;
}
},
cw(f, o) { return f == 'back' ? doCcwO(o) : doCwO(o); },
ccw(f, o) { return f == 'back' ? doCwO(o) : doCcwO(o); }
};
export function applyMoves(p: Place, ms: Rotation[]): Place {
return ms.reduce(([f, o], m) => [moveF[m](f), moveO[m](f, o)], p);
}
/** the sequence of movements to put this place on the front */
export function toFrontUpright([f, o]: Place): [RotateXY[], RotateZ[]] {
const toFront: (f: Face) => RotateXY[] =
table<Face, RotateXY[]>({
front: [], top: ['down'], back: ['left', 'left'],
bottom: ['up'], left: ['right'], right: ['left']
});
const toUpright: (o: Orientation) => RotateZ[] =
table<Orientation, RotateZ[]>({
up: [], left: ['cw'], down: ['cw', 'cw'], right: ['ccw']
});
const directions = toFront(f);
const rotations = toUpright(applyMoves([f, o], directions)[1]);
return [directions, rotations];
}
const movementToTransform = table<Rotation, string>({
up: 'rotateX(.25turn)', down: 'rotateX(-.25turn)',
left: 'rotateY(-.25turn)', right: 'rotateY(.25turn)',
cw: 'rotateZ(.25turn)', ccw: 'rotateZ(-.25turn)'
});
/** the css `transform` value corresponding to this sequence of movements */
export function movementsToTransform(ms: Rotation[]): string {
return ms.length > 0 ? ms.map(movementToTransform).join(' ') : 'none';
}
const faceToTransform = table<Face, string>({
front: '', top: 'rotateX(.25turn)',
back: 'rotateX(.5turn)', bottom: 'rotateX(-.25turn)',
left: 'rotateY(-.25turn)', right: 'rotateY(.25turn)'
});
const orientationToTransform = table<Orientation, string>({
up: '', left: 'rotateZ(-.25turn)',
down: 'rotateZ(-.5turn)', right: 'rotateZ(.25turn)'
});
/**
* the css `transform` value that will bring the given place to the
* front and upright
*/
export function placeToTransform([f, o]: Place): string {
const ft = faceToTransform(f);
const ot = orientationToTransform(o);
return ft || ot ? `${ft} ${ot}` : 'none';
}
/** a map of where each pane is on the cube */
export type Conf = Record<Pane, Place>;
/** initial cube configuration with `hello` at the front */
let current: Conf = {
hello: ['front', 'up'], id: ['left', 'up'],
activities: ['back', 'down'], links: ['right', 'up'],
friends: ['bottom', 'up'], six: ['top', 'up'],
};
// the back face is 'down' so it has the same visual orientation as other side
// faces
/** apply the css transforms to each pane element */
export function applyConfiguration(): void {
for (const pane of allPanes) {
const element = document.getElementById(pane)!;
const place = current[pane];
element.style.setProperty('--base-transform', placeToTransform(place));
element.style.pointerEvents = place[0] == 'front' ? 'auto' : 'none';
}
}
export function move(c: Conf, ...ms: Rotation[]): Conf {
let res: Partial<Conf> = {};
for (const pane of allPanes) { res[pane] = applyMoves(c[pane], ms) }
return res as Conf;
}
export function animateMoveWith(ds: RotateXY[], rs: RotateZ[]): void {
const outer = document.getElementById('outer')!;
const cube = document.getElementById('cube')!;
cube.dataset.moving = 'true';
cube.style.transition = '0.4s cubic-bezier(.4, -0.29, .43, 1.26)';
outer.style.transition = `0.4s 0.25s cubic-bezier(.48, 0, .44, 1.07)`;
function transitionListener(elem: HTMLElement): () => void {
function handler(e: Event) { if (e.target == elem) { finish(); } }
elem.addEventListener('transitionend', handler);
return () => elem.removeEventListener('transitionend', handler);
}
let removeOuter = () => {};
let removeCube = () => {};
if (rs.length > 0) {
removeOuter = transitionListener(outer);
cube.style.transform = movementsToTransform(ds);
outer.style.transform = movementsToTransform(rs);
} else if (ds.length > 0) {
removeCube = transitionListener(cube);
cube.style.transform = movementsToTransform(ds);
} else {
finish();
}
function finish() {
removeOuter(); removeCube();
delete cube.dataset.moving;
outer.style.transition = cube.style.transition = 'none';
outer.style.transform = cube.style.transform = 'none';
current = move(current, ...ds, ...rs);
applyConfiguration();
}
}
export function animateMoveTo(pane: Pane): void {
animateMoveWith(...toFrontUpright(current[pane]));
history.replaceState(null, '🦎', `#${pane}`);
}
export function squashCube() {
for (const pane of allPanes) {
const elem = document.getElementById(pane)!
elem.style.setProperty('--base-transform', 'none');
}
}
export function instantMoveTo(pane: Pane): void {
current = move(current, ...toFrontUpright(current[pane]).flat());
applyConfiguration();
}
export function LEFT() { animateMoveWith(['left'], []); }
export function RIGHT() { animateMoveWith(['right'], []); }
export function UP() { animateMoveWith(['up'], []); }
export function DOWN() { animateMoveWith(['down'], []); }
export function CW() { animateMoveWith([], ['cw']); }
export function CCW() { animateMoveWith([], ['ccw']); }
}
namespace Flat {
export function fadeTo(newPane: Pane): void {
for (const pane of allPanes) {
const here = pane == newPane;
document.getElementById(pane)!.dataset.selected = `${here}`;
if (here) { history.replaceState(null, '🦎', `#${pane}`); }
}
}
}
let reducedMotion =
matchMedia(`(prefers-reduced-motion: reduce),
(max-height: 649px), (max-width: 649px)`);
function switchTo(pane: Pane): void {
if (reducedMotion.matches) { Cube.instantMoveTo(pane); }
else { Cube.animateMoveTo(pane); }
Flat.fadeTo(pane);
}
function setup(): void {
const here = location.hash.slice(1) || 'hello';
for (const pane of allPanes) {
const box = document.getElementById(`b-${pane}`) as HTMLInputElement;
box.addEventListener('change', () => {
if (box.checked) { switchTo(pane); }
});
if (pane == here) {
Cube.instantMoveTo(pane);
Flat.fadeTo(pane);
box.checked = true;
}
}
}
document.addEventListener('DOMContentLoaded', setup);
export {};