A more advanced visualiser that uses 3D CSS transforms to highlight depth.
Note: This is the original work of Chris Price, lightly updated by me to use modern CSS & JS.
View source
interface Face {
x: number;
y: number;
z: number;
w: number;
h: number;
r: number;
c: string;
}
export function explodz(window: Window): void {
const STEP = 25;
const STEP_DELTA = 0.001;
const PERSPECTIVE = 5000;
const COLOURS = [
"#C33",
"#ea4c88",
"#663399",
"#0066cc",
"#669900",
"#ffcc33",
"#ff9900",
"#996633",
];
function getColour(depth: number): string {
return COLOURS[depth % (COLOURS.length - 1)];
}
function getFaceHTML({ x, y, z, w, h, r, c }: Face): string {
const visual = `background-color:${c};`;
const dimensions = `width:${w}px; height:${h}px;`;
const transform = `transform:
translate3d(${x}px,${y}px,${z}px)
rotateX(270deg)
rotateY(${r}deg);
`;
return (
"<div class='explodz-face' style='" +
visual +
dimensions +
transform +
"'></div>"
);
}
let facesHTML = "";
let mode = "FACES";
function traverse(
element: HTMLElement,
depth: number,
offsetLeft: number,
offsetTop: number
): void {
const childNodes = element.childNodes as NodeListOf<HTMLElement>;
const l = childNodes.length;
let index = 0;
for (const childNode of childNodes) {
if (childNode.nodeType === Node.ELEMENT_NODE) {
const translateZ = (STEP + (l - index) * STEP_DELTA).toFixed(3);
childNode.classList.add("explodz-element");
childNode.style.setProperty("--z", translateZ + "px");
let elementBodyOffsetLeft = offsetLeft;
let elementBodyOffsetTop = offsetTop;
if (childNode.offsetParent === element) {
elementBodyOffsetLeft += element.offsetLeft;
elementBodyOffsetTop += element.offsetTop;
}
traverse(
childNode,
depth + 1,
elementBodyOffsetLeft,
elementBodyOffsetTop
);
const colour = getColour(depth);
const stepDepth = (depth + 1) * STEP;
facesHTML += getFaceHTML({
x: elementBodyOffsetLeft + childNode.offsetLeft,
y: elementBodyOffsetTop + childNode.offsetTop,
z: stepDepth,
w: childNode.offsetWidth,
h: STEP,
r: 0,
c: colour,
});
facesHTML += getFaceHTML({
x:
elementBodyOffsetLeft +
childNode.offsetLeft +
childNode.offsetWidth,
y: elementBodyOffsetTop + childNode.offsetTop,
z: stepDepth,
w: childNode.offsetHeight,
h: STEP,
r: 270,
c: colour,
});
facesHTML += getFaceHTML({
x: elementBodyOffsetLeft + childNode.offsetLeft,
y:
elementBodyOffsetTop + childNode.offsetTop + childNode.offsetHeight,
z: stepDepth,
w: childNode.offsetWidth,
h: STEP,
r: 0,
c: colour,
});
facesHTML += getFaceHTML({
x: elementBodyOffsetLeft + childNode.offsetLeft,
y: elementBodyOffsetTop + childNode.offsetTop,
z: stepDepth,
w: childNode.offsetHeight,
h: STEP,
r: 270,
c: colour,
});
index++;
}
}
}
const yCenter = (window.innerHeight / 2).toFixed(2);
const body = document.body;
body.classList.add("explodz-body");
traverse(body, 0, 0, 0);
const styleNode = document.createElement("style");
styleNode.innerHTML = `
.explodz-body {
transition: transform 0.5s;
overflow: visible;
perspective: ${PERSPECTIVE}px;
perspective-origin: center ${yCenter}px;
transform-style: preserve-3d;
transform-origin: center ${yCenter}px;
}
.explodz-body .explodz-element {
overflow: visible;
transform-style: preserve-3d;
transform: translateZ(var(--z))
}
.explodz-faces {
position: absolute;
top: 0;
left: 0;
}
.explodz-face {
position: absolute;
transform-origin: 0 0 0;
}
`;
body.appendChild(styleNode);
const faces = document.createElement("div");
faces.classList.add("explodz-faces");
faces.innerHTML = facesHTML;
body.appendChild(faces);
function onMouseMove(e: MouseEvent) {
const xrel = e.clientX / window.innerWidth;
const yrel = 1 - e.clientY / window.innerHeight;
const rx = yrel * 120 - 60;
const ry = xrel * 120 - 60;
body.style.transform = `rotateX(${rx.toFixed(2)}deg) rotateY(${ry.toFixed(
2
)}deg)`;
}
function toggleFaces() {
mode = mode === "FACES" ? "NO_FACES" : "FACES";
faces.style.display = mode === "FACES" ? "block" : "none";
}
function disable(event: KeyboardEvent) {
if (event.code.toLowerCase() === "escape") {
faces.innerHTML = "";
body.classList.remove("explodz-body");
body.style.transform = "";
const explodzEls: NodeListOf<HTMLDivElement> =
document.querySelectorAll(".explodz-element");
for (const explodzEl of explodzEls) {
explodzEl.classList.remove("explodz-element");
explodzEl.style.transform = "";
}
styleNode.remove();
document.removeEventListener("mousemove", onMoveDebounced, true);
document.removeEventListener("mouseup", toggleFaces, true);
document.removeEventListener("keydown", disable, true);
}
}
function debounce(func: (...args: never) => never | void, timeout = 300) {
let timer: NodeJS.Timeout;
return (...args: never) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
};
}
const onMoveDebounced = debounce(onMouseMove, 100);
document.addEventListener("mousemove", onMoveDebounced, true);
document.addEventListener("mouseup", toggleFaces, true);
document.addEventListener("keydown", disable, true);
}