Skip to content

Instantly share code, notes, and snippets.

@OrionReed
Last active April 26, 2024 16:59
Show Gist options
  • Save OrionReed/4c3778ebc2b5026d2354359ca49077ca to your computer and use it in GitHub Desktop.
Save OrionReed/4c3778ebc2b5026d2354359ca49077ca to your computer and use it in GitHub Desktop.
3D DOM viewer, copy-paste this into your console to visualise the DOM topographically.
// 3D Dom viewer, copy-paste this into your console to visualise the DOM as a stack of solid blocks.
// You can also minify and save it as a bookmarklet (https://www.freecodecamp.org/news/what-are-bookmarklets/)
(() => {
const SHOW_SIDES = false; // color sides of DOM nodes?
const COLOR_SURFACE = true; // color tops of DOM nodes?
const COLOR_RANDOM = false; // randomise color?
const COLOR_HUE = 190; // hue in HSL (https://hslpicker.com)
const MAX_ROTATION = 180; // set to 360 to rotate all the way round
const THICKNESS = 20; // thickness of layers
const DISTANCE = 10000; // ¯\\_(ツ)_/¯
function getRandomColor() {
const hue = Math.floor(Math.random() * 360);
const saturation = 50 + Math.floor(Math.random() * 30);
const lightness = 40 + Math.floor(Math.random() * 30);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
const getDOMDepth = element => [...element.children].reduce((max, child) => Math.max(max, getDOMDepth(child)), 0) + 1;
const domDepthCache = getDOMDepth(document.body);
const getColorByDepth = (depth, hue = 0, lighten = 0) => `hsl(${hue}, 75%, ${Math.min(10 + depth * (1 + 60 / domDepthCache), 90) + lighten}%)`;
// Apply initial styles to the body to enable 3D perspective
const body = document.body;
body.style.overflow = "visible";
body.style.transformStyle = "preserve-3d";
body.style.perspective = DISTANCE;
const perspectiveOriginX = (window.innerWidth / 2);
const perspectiveOriginY = (window.innerHeight / 2);
body.style.perspectiveOrigin = body.style.transformOrigin = `${perspectiveOriginX}px ${perspectiveOriginY}px`;
traverseDOM(body, 0, 0, 0);
document.addEventListener("mousemove", (event) => {
const rotationY = (MAX_ROTATION * (1 - event.clientY / window.innerHeight) - (MAX_ROTATION / 2));
const rotationX = (MAX_ROTATION * event.clientX / window.innerWidth - (MAX_ROTATION / 2));
body.style.transform = `rotateX(${rotationY}deg) rotateY(${rotationX}deg)`;
});
// Create side faces for an element to give it a 3D appearance
function createSideFaces(element, color) {
if (!SHOW_SIDES) { return }
const width = element.offsetWidth;
const height = element.offsetHeight;
const fragment = document.createDocumentFragment();
// Helper function to create and style a face
const createFace = ({ width, height, transform, transformOrigin, top, left, right, bottom }) => {
const face = document.createElement('div');
face.className = 'dom-3d-side-face';
Object.assign(face.style, {
transformStyle: "preserve-3d",
backfaceVisibility: 'hidden',
position: 'absolute',
width: `${width}px`,
height: `${height}px`,
background: color,
transform,
transformOrigin,
overflow: 'hidden',
willChange: 'transform',
top,
left,
right,
bottom
});
fragment.appendChild(face);
}
// Top face
createFace({
width,
height: THICKNESS,
transform: `rotateX(-270deg) translateY(${-THICKNESS}px)`,
transformOrigin: 'top',
top: '0px',
left: '0px',
});
// Right face
createFace({
width: THICKNESS,
height,
transform: 'rotateY(90deg)',
transformOrigin: 'left',
top: '0px',
left: `${width}px`
});
// Bottom face
createFace({
width,
height: THICKNESS,
transform: `rotateX(-90deg) translateY(${THICKNESS}px)`,
transformOrigin: 'bottom',
bottom: '0px',
left: '0px'
});
// Left face
createFace({
width: THICKNESS,
height,
transform: `translateX(${-THICKNESS}px) rotateY(-90deg)`,
transformOrigin: 'right',
top: '0px',
left: '0px'
});
element.appendChild(fragment);
}
// Recursive function to traverse child nodes, apply 3D styles, and create side faces
function traverseDOM(parentNode, depthLevel, offsetX, offsetY) {
for (let children = parentNode.childNodes, childrenCount = children.length, i = 0; i < childrenCount; i++) {
const childNode = children[i];
if (!(1 === childNode.nodeType && !childNode.classList.contains('dom-3d-side-face'))) continue;
const color = COLOR_RANDOM ? getRandomColor() : getColorByDepth(depthLevel, COLOR_HUE, -5);
Object.assign(childNode.style, {
transform: `translateZ(${THICKNESS}px)`,
overflow: "visible",
backfaceVisibility: "hidden",
isolation: "auto",
transformStyle: "preserve-3d",
backgroundColor: COLOR_SURFACE ? color : getComputedStyle(childNode).backgroundColor,
willChange: 'transform',
});
let updatedOffsetX = offsetX;
let updatedOffsetY = offsetY;
if (childNode.offsetParent === parentNode) {
updatedOffsetX += parentNode.offsetLeft;
updatedOffsetY += parentNode.offsetTop;
}
createSideFaces(childNode, color);
traverseDOM(childNode, depthLevel + 1, updatedOffsetX, updatedOffsetY);
}
}
})()
@qwell
Copy link

qwell commented Mar 27, 2024

If you change the mousemove event listener to the following, it'll use just the client area for rotation instead of the entire screen.

document.addEventListener("mousemove", (event) => {
    const rotationY = (MAX_ROTATION * (1 - event.pageY / window.innerHeight) - (MAX_ROTATION / 2));
    const rotationX = (MAX_ROTATION * event.pageX / window.innerWidth - (MAX_ROTATION / 2));
    body.style.transform = `rotateX(${rotationY}deg) rotateY(${rotationX}deg)`;
});

Edit: Scrolling makes it not work well. Probably use something other than pageX/pageY. YMMV

@OrionReed
Copy link
Author

Improvements/Fixes:

  • COLOR_HUE now changes the color
  • Using clientX/Y instead of pageX/Y to handle scrolling better
  • Rotation now uses window.innerHeight/Width to work with arbitrary browser sizes and second monitors

Thanks @EyeOfMidas and @qwell!

@OrionReed
Copy link
Author

OrionReed commented Mar 27, 2024

FYI you can make a bookmarklet in Firefox/Chrome and add this as the url to create a 1-click way of loading this into any site.

javascript:(()=>{function e(){return`hsl(${Math.floor(360*Math.random())}, ${50+Math.floor(30*Math.random())}%, ${40+Math.floor(30*Math.random())}%)`}let t=e=>[...e.children].reduce((e,n)=>Math.max(e,t(n)),0)+1,n=t(document.body),r=(e,t=0,r=0)=>`hsl(${t}, 75%, ${Math.min(10+e*(1+60/n),90)+r}%)`,o=document.body;o.style.overflow="visible",o.style.transformStyle="preserve-3d",o.style.perspective=1e4;let i=window.innerWidth/2,l=window.innerHeight/2;function s(e,t){}function f(e,t,n,o){for(let i=e.childNodes,l=i.length,$=0;$<l;$++){let a=i[$];if(!(1===a.nodeType&&!a.classList.contains("dom-3d-side-face")))continue;let d=r(t,190,-5);Object.assign(a.style,{transform:"translateZ(20px)",overflow:"visible",backfaceVisibility:"hidden",transformStyle:"preserve-3d",backgroundColor:d,willChange:"transform"});let c=n,m=o;a.offsetParent===e&&(c+=e.offsetLeft,m+=e.offsetTop),s(a,d),f(a,t+1,c,m)}}o.style.perspectiveOrigin=o.style.transformOrigin=`${i}px ${l}px`,f(o,0,0,0),document.addEventListener("mousemove",e=>{let t=180*(1-e.clientY/window.innerHeight)-90,n=180*e.clientX/window.innerWidth-90;o.style.transform=`rotateX(${t}deg) rotateY(${n}deg)`})})();

@Mte90
Copy link

Mte90 commented Mar 27, 2024

Convert it as browser extension is not better?
So we can get updates automatically?

@iacore
Copy link

iacore commented Mar 27, 2024

I don't think it works in Firefox when the page is scrollable. The viewing angle is calculated wrong.

@Mte90
Copy link

Mte90 commented Mar 27, 2024

I tried on Firefox and works with the scroll etc.
The only problem to me is just block the mouse movement that is not happening with esc or other buttons

@aradalvand
Copy link

Doesn't work in Chrome.

@OrionReed
Copy link
Author

@aradalvand yeah I'm aware it looks super buggy in Chrome, not sure what's happening there yet.

@immortalx74
Copy link

This is one of the coolest things I've seen in a while!
Works great in Firefox.

@larsgw
Copy link

larsgw commented Mar 27, 2024

I'm having some issues with SVGs, but that's to be expected honestly.

@ylluminate
Copy link

ylluminate commented Mar 27, 2024

@OrionReed t'would be nice to see this converted to a userscript similar to how https://gist.github.com/hf02/2f2fb776ba233fd758af559b9de9e177 has done and then this would facilitate easy/auto updates through Tampermonkey. The downside of using forked scripts is that they don't get. your TLC since you created it and it appears are passionate about improvements...

@greatsage-raphael
Copy link

This is mad cool

@OrionReed
Copy link
Author

As requested, I've made a browser extension. A little development help would be massive — currently only works in Firefox so Chrome/Safari help would be great. Once it's polished, I'll make it freely available in extension stores for easy usage.

I will continue to update this gist with improvements from that repo so that you can always just copy-paste it into your console.

@krzentner
Copy link

This is very cool, thanks for sharing it. One thing to note is that it breaks inside elements that have the style isolation: isolate;. I think it's probably best to force isolation: auto; on all elements when using this.

@zardoz03
Copy link

zardoz03 commented Mar 28, 2024

does anyone want a cursed self minifying header? :D
requires terser or a clipboard tool that allows stdin to be piped into it to be within yr path:

#!/bin/zsh
//(){ : ;}
//;set +xe # set to -xe if you need to debug
//;NEW="$(printf $0 | sed 's/\.js//g')".min.js
//;compress_opt(){ terser --compress ecma=5,computed_props=false $@ }
//;clean_opt(){ compress_opt  $@ }
//;run(){ clean_opt --keep-classnames --keep-fnames -- $@ }
//;run $0 | grep -v '#!/bin/zsh' > $NEW
//;{ printf 'javascript:' ;cat $NEW ;} | vis-clipboard --copy
//;exit 0

ERRATA: note clean_opt is technically redundant, however it can be used for passing other options to terser
Also there is probably another way to do args passing, however it just works this way :)

@zzdoreen
Copy link

cool

@MrZhouZh
Copy link

so cool

@DrSensor
Copy link

Better to change mousemove with pointermove to make it works on touchscreen. Though it will be bit annoying to scroll on mobile phone.
Another way to make it seamless on mobile phone is by handle deviceorientation event then use: alpha, beta, gamma; value 🤔

@yagnikvamja
Copy link

Great..! it was cool..

@jonrandy
Copy link

jonrandy commented Mar 28, 2024

Anyone remember when Firefox had a far better version of this built in? https://www.youtube.com/watch?v=zqHV625EU3E

@Mte90
Copy link

Mte90 commented Mar 28, 2024

Anyone remember when Firefox had this built in?

yes

@OrionReed
Copy link
Author

@krzentner could you give an example of the isolation: isolate issue? Happy to make the change once I see what it's doing.

@ylluminate
Copy link

Everyone: since @OrionReed is now developing browser extensions (https://github.com/OrionReed/dom3d), it might be best to file issues at https://github.com/OrionReed/dom3d/issues - even if they are referenced here.

I think it might make it easier to track and resolve specific things.

@OrionReed
Copy link
Author

@ylluminate ^ absolutely. we can push improvement from that repo into this single-function gist and reference those issues/changes here.

@malikalimoekhamedov
Copy link

This should be a browser plugin.

@swnck
Copy link

swnck commented Mar 29, 2024

I have a updated version without the blue color:
https://gist.github.com/swnck/7aa80d2968a115ada8d920d772823cc8

@OrionReed
Copy link
Author

@malikalimoekhamedov it is being plugin-ified here here

@malikalimoekhamedov
Copy link

@malikalimoekhamedov it is being plugin-ified here here

Beautiful

@OrionReed
Copy link
Author

I've added some of the few major issues to the extension repo and if anyone can fix any of them I will buy you a coffee or drink of your choice!

Specifically, there are 3 issues that are causing the majority of problems:

@OrionReed
Copy link
Author

Revised the gist, should now work on many more websites which previously looked flat thanks to forcing isolation: auto

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment