POST: Simulating Lighting with CSS Perspective Origin and Shadows

Simulating Lighting with CSS Perspective Origin and Shadows

A while back I put together a demo for a friend on how you can use CSS pseudo elements as shadows of their parents. I've put together a demo explaining how to achieve this CSS Trick with the addition of dynamic perspective.

Some important things to remember when coding anything with a mousemove listener:

  1. Be sure your element exists! Initializing at the end of the body or inside an window onload event (we'll use this method in the demo)
  2. Ensure all child elements (or elements overlapping the world) have no pointer events

To address the first point we will listen for the load event on the window object like this:

window.addEventListener('load', e => { /* our code to execute on load here */ });

And to address the second point we will add a pointer-events property to all DOM elements that may interfer with our mousemove event (setting the pointer-events to none disables all events that are normally broadcast from a DOM element:

pointer-events: none;

Some other 3D related things you should know:

The transform-origin property is used to ensure the shadow bottom lines up with it's parent's bottom when rotated.

Anything in the stage area should be centered and absolutely positioned. This is done by setting a width and height along with a counter margin-left and margin-top and then setting the top and left values to 50%; for example if our element was 100px x 100px the setup would look like this

position:absolute;
width:100px;
height:100px;
margin-left:-50px; /* half the width multiplied by-1 */
margin-top:-50px; /* half the height multiplied by -1 */
left:50%;
top:50%;

Also, because we are adding a 3D shadow as a pseudo element our parent element needs an additional property set:

transform-style:preserve-3d;

Everything else is going to pretty much basic CSS. So lets put it all together:

<div class="world">
    <div class="ground">
    </div>
    <div class="sign hasshadow">
    </div>
</div>

And the CSS:

.sign{
    pointer-events:none;
    background-image:url('https://sleekinteractive.com/assets/clipart.sign.png');
    background-size:contain;
    height:100px;
    width:100px;
    position:absolute;
    left:50%;
    top:50%;
    margin-top:-50px;
    margin-left:-50px;
    transform:translateZ(-60px);
}
.hasshadow{
    transform-style:preserve-3d;
}
.hasshadow:after{
    content:"";
    position:absolute;
    width:100%;
    height:100%;
    background-image:inherit;
    display:block;
    background-size:inherit;
    transform: rotateX(90deg);
    filter: brightness(0.1);
    transform-origin:50% 100%;
    backface-visibility:hidden;
}
.ground{
    width:2000px;
    height:2000px;
    border-radius:50%;
    background-color:#061906;
    transform:rotateX(90deg);
    position:absolute;
    left:50%;
    top:50%;
    margin-top:-1000px;
    margin-left:-1000px;
    pointer-events:none;
}
.world{
    background-color:#000;
    position:relative;
    perspective:100px;
    perspective-origin:30% 20%;
    height:400px;
    overflow:hidden;
}
.ground:after{
    content:"";
    display:block;
    height:100%;
    width:100%;
    background-size:100% 100%;
    background: radial-gradient(circle, rgba(239,246,14,0.2) 0%, rgba(0,0,0,1) 77%);
    border-radius:50%;
}

And now we'll add our Javascript onload listener as well as a mousemove listener for our stage.

var world;

window.addEventListener('load', e => {

    world = document.querySelector('.world');

    world.addEventListener('mousemove', e => {

        var xPercentage = (e.offsetX/window.innerWidth)*100;
        var yPercentage = (e.offsetY/window.innerHeight)*100;

        world.style.perspectiveOrigin = xPercentage + '% ' + Math.min(yPercentage,40) + '%';

    });
});