Introduction to 3D in CSS
H E L P !
This article is a modest introduction to 3D in CSS through an example. I share with you how I made a Minecraft creeper head in CSS and animated it with JavaScript. This article is composed of 2 parts:
Modeling a Minecraft creeper head.
Making the creeper alive.
Modeling the creeper head is like modeling a cube. I use 2 assets:
The face of the creeper,
face.avif
The side of his head,
side.avif
Each side of the cube is represented by an img
tag and is nested in a div
element. We need 6 images for the 6 square faces:
By default, HTML elements are positioned in a two-dimensional space, meaning that everything is flat
.
transform-style
is a CSS property indicating the way children elements are positioned in the space. With the keyword preserve-3d
, we unlock the Z dimension and add some depth to the page. We can now position elements relative to the X, Y and Z axis. The CSS function translateZ
position an element closer or farther away from the viewer:
The following figure can help you understand how the elements are positioned in the 3D space:
This part is optional. The modeling is over, good job Jackson. The character looks very static though: I would like him to follow the cursor for more user interaction. To do so, we need to compute 2 angles. Once calculated, we can use rotateX
and rotateY
to position the cube relative to the axis of rotation X and Y. It has to be done in JavaScript:
<div class="creeper" id="demo">
<img src="face.avif" alt="" class="side front" />
<img src="side.avif" alt="" class="side back" />
<img src="side.avif" alt="" class="side top" />
<img src="side.avif" alt="" class="side bottom" />
<img src="side.avif" alt="" class="side right" />
<img src="side.avif" alt="" class="side left" />
</div>
.creeper {
--size: 128px;
--half-size: calc(var(--size) / 2);
margin: 0 auto;
width: var(--size);
height: var(--size);
/* 👇 Elements are now positioned in a 3D space */
transform-style: preserve-3d;
/* Make the animation smoother */
transition: 0.05s transform ease-out;
transform: rotateX(var(--angle-x, 0deg)) rotateY(var(--angle-y, 0deg));
}
.side {
position: absolute;
width: var(--size);
height: var(--size);
}
.front {
transform: translateZ(var(--half-size));
}
.back {
transform: translateZ(calc(var(--half-size) * -1));
}
.top {
transform: translateY(calc(var(--half-size) * -1)) rotateX(90deg);
}
.left {
transform: translateX(calc(var(--half-size) * -1)) rotateY(90deg);
}
.bottom {
transform: translateY(var(--half-size)) rotateX(90deg);
}
.right {
transform: translateX(var(--half-size)) rotateY(90deg);
}
type Options = {
maxAngleX: number
maxAngleY: number
}
const map = (value: number, low1: number, high1: number, low2: number, high2: number) => low2 + ((high2 - low2) * (value - low1)) / (high1 - low1)
function rotate(element: HTMLElement, angleX: number, angleY: number): void {
element.style.setProperty('--angle-x', `${angleX}deg`)
element.style.setProperty('--angle-y', `${angleY}deg`)
}
function handleOrientation(element: HTMLElement, options: Options): void {
window.addEventListener('deviceorientation', (event: DeviceOrientationEvent) => {
const decY = map((event.gamma || 0) + 180, 0, 360, -options.maxAngleY, options.maxAngleY)
rotate(element, 0, -decY)
})
}
function handleCursor(element: HTMLElement, options: Options): void {
window.addEventListener('mousemove', (event: MouseEvent) => {
const elementRect = element.getBoundingClientRect()
const originX = elementRect.left + elementRect.width / 2
const originY = elementRect.top + elementRect.height / 2
const width = window.innerWidth
const height = window.innerHeight
const angleX = map(event.clientY - originY, -height / 2, height / 2, -options.maxAngleY, options.maxAngleY)
const angleY = map(event.clientX - originX, -width / 2, width / 2, -options.maxAngleX, options.maxAngleX)
rotate(element, -angleX, angleY)
})
}
const creeper = document.querySelector('#demo') as HTMLElement
const options: Options = { maxAngleX: 40, maxAngleY: 20 }
// For touch screens
if (window.DeviceOrientationEvent && 'ontouchstart' in window) {
handleOrientation(creeper, Object.assign({}, options, { maxAngleY: 80 }))
} else {
handleCursor(creeper, options)
}