/** 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(m: Record): (x: A) => B { return x => m[x]; } const doCwO = table({up: 'right', right: 'down', down: 'left', left: 'up'}); const doCcwO = table({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 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 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({ front: [], top: ['down'], back: ['left', 'left'], bottom: ['up'], left: ['right'], right: ['left'] }); const toUpright: (o: Orientation) => RotateZ[] = table({ 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({ 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({ front: '', top: 'rotateX(.25turn)', back: 'rotateX(.5turn)', bottom: 'rotateX(-.25turn)', left: 'rotateY(-.25turn)', right: 'rotateY(.25turn)' }); const orientationToTransform = table({ 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; /** 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 = {}; 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 {};