335 lines
9.6 KiB
TypeScript
335 lines
9.6 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 NO_TRANS = 'rotateY(0deg)';
|
|
|
|
|
|
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(' ') : NO_TRANS;
|
|
}
|
|
|
|
|
|
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}` : NO_TRANS;
|
|
}
|
|
|
|
|
|
/** 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.inert = place[0] != 'front';
|
|
}
|
|
}
|
|
|
|
export function move(c: Conf, ...ms: Rotation[]): Conf {
|
|
const 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')!;
|
|
|
|
console.log('animateMoveWith');
|
|
|
|
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) {
|
|
console.log('xy');
|
|
|
|
removeOuter = transitionListener(outer);
|
|
cube.style.transform = movementsToTransform(ds);
|
|
outer.style.transform = movementsToTransform(rs);
|
|
} else if (ds.length > 0) {
|
|
console.log('z');
|
|
|
|
removeCube = transitionListener(cube);
|
|
cube.style.transform = movementsToTransform(ds);
|
|
} else {
|
|
finish();
|
|
}
|
|
|
|
function finish() {
|
|
console.log('finish');
|
|
|
|
removeOuter(); removeCube();
|
|
|
|
delete cube.dataset.moving;
|
|
outer.style.transition = cube.style.transition = 'none';
|
|
outer.style.transform = cube.style.transform = NO_TRANS;
|
|
|
|
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', NO_TRANS);
|
|
}
|
|
}
|
|
|
|
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 elem = document.getElementById(pane)!;
|
|
if (pane == newPane) {
|
|
// show new pane
|
|
//
|
|
// - "entering" state is displayed and z-index 1, but opacity 0
|
|
// - "active" state is fully showing
|
|
//
|
|
// "entering" exists because firefox doesn't animate a
|
|
// transition out of `display: none`
|
|
elem.dataset.state = 'entering';
|
|
setTimeout(() => elem.dataset.state = 'active');
|
|
history.replaceState(null, '🦎', `#${pane}`);
|
|
} else if (elem.dataset.state == 'active') {
|
|
// hide old pane
|
|
//
|
|
// - "leaving" is z-index -1 and opacity 0
|
|
// - "hidden" is `display: none`
|
|
//
|
|
// separate for the same reason as above
|
|
function hide() { elem.dataset.state = 'hidden'; }
|
|
elem.addEventListener('transitionend', hide, {once: true});
|
|
elem.dataset.state = 'leaving';
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
const 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;
|
|
} else {
|
|
const elem = document.getElementById(pane)!;
|
|
elem.dataset.state = 'hidden';
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', setup);
|
|
|
|
export {};
|