258 lines
7.2 KiB
TypeScript
258 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 {};
|