yummy.cricket/cube.ts

257 lines
7.2 KiB
TypeScript

type Face = 'front' | 'top' | 'back' | 'bottom' | 'left' | 'right';
type Pane = 'hello' | 'id' | 'activities' | 'links' | 'friends' | 'six';
const panes: Pane[] = ['hello', 'id', 'activities', 'links', 'friends', 'six'];
/**
* - 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
*/
type Orientation = 'up' | 'left' | 'down' | 'right';
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'});
type Direction = 'up' | 'left' | 'down' | 'right';
type Rotation = 'cw' | 'ccw';
type Movement = Direction | Rotation;
// if you rotate the cube "up" (along the x axis), face f becomes up(f)
// et cetera
const faceMoves: Record<Movement, (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 orientationMoves:
Record<Movement, (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); }
};
function applyMoves([f, o]: Place, ms: Movement[]): Place {
for (const m of ms) {
o = orientationMoves[m](f, o);
f = faceMoves[m](f);
}
return [f, o];
}
const toFront = table<Face, Direction[]>({
front: [], top: ['down'], back: ['left', 'left'],
bottom: ['up'], left: ['right'], right: ['left']
});
const toUpright = table<Orientation, Rotation[]>({
up: [], left: ['cw'], down: ['cw', 'cw'], right: ['ccw']
});
function toFrontUpright([f, o]: Place): [Direction[], Rotation[]] {
const directions = toFront(f);
const rotations = toUpright(applyMoves([f, o], directions)[1]);
return [directions, rotations];
}
const movementToTransform = table<Movement, string>({
up: 'rotateX(.25turn)', down: 'rotateX(-.25turn)',
left: 'rotateY(-.25turn)', right: 'rotateY(.25turn)',
cw: 'rotateZ(.25turn)', ccw: 'rotateZ(-.25turn)'
});
function movementsToTransform(ms: Movement[]) {
return ms.map(movementToTransform).join(' ');
}
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)'
});
function toTransform(f: Face, o: Orientation): string {
const ft = faceToTransform(f);
const ot = orientationToTransform(o);
return ft || ot ? `${ft} ${ot}` : 'none';
}
type Conf = Record<Pane, Place>;
// the back face is 'down' so it has the same visual orientation as other side faces
let current: Conf = {
hello: ['front', 'up'], id: ['left', 'up'],
activities: ['back', 'down'], links: ['right', 'up'],
friends: ['bottom', 'up'], six: ['top', 'up'],
};
function applyConfiguration(): void {
for (const pane of panes) {
const element = document.getElementById(pane)!;
const [face, ori] = current[pane];
element.style.setProperty('--base-transform', toTransform(face, ori));
element.style.pointerEvents = face == 'front' ? 'auto' : 'none';
}
}
function move(c: Conf, ...ms: Movement[]): Conf {
let res: Partial<Conf> = {};
for (const p of panes) { res[p] = applyMoves(c[p], ms) }
return res as Conf;
}
function animateMoveWith(ds: Direction[], rs: Rotation[]): 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();
outer.style.transition = cube.style.transition =
outer.style.transform = cube.style.transform = 'none';
delete cube.dataset.moving;
current = move(current, ...ds, ...rs);
applyConfiguration();
}
}
function animateMoveTo(pane: Pane): void {
const [ds, rs] = toFrontUpright(current[pane]);
animateMoveWith(ds, rs);
history.replaceState(null, '', `#${pane}`);
}
function setup(): void {
for (const pane of panes) {
const box = document.getElementById(`b-${pane}`) as HTMLInputElement;
box.addEventListener('change', () => {
if (box.checked) { animateMoveTo(pane); }
});
if (location.hash == `#${pane}`) {
current = move(current, ...toFrontUpright(current[pane]).flat());
box.checked = true;
}
}
applyConfiguration();
}
document.addEventListener('DOMContentLoaded', setup);
/*
function LEFT() { animateMoveWith(['left'], []); }
function RIGHT() { animateMoveWith(['right'], []); }
function UP() { animateMoveWith(['up'], []); }
function DOWN() { animateMoveWith(['down'], []); }
function CW() { animateMoveWith([], ['cw']); }
function CCW() { animateMoveWith([], ['ccw']); }
*/
export {};