Building a custom loading screen
In an ideal world, your site’s visitors would all have fiber-like internet speeds and web pages would download in the blink of an eye. Unfortunately, slow connections and long loading times are unavoidable for many users, so having a good loading screen while your 3D assets download is critical to your site’s overall experience.
While Dopple does provide some ready-made, animated loading screens, you may want to create your own.
In this guide, we’ll walk you through building a custom loader, listening for when your 3D assets have finished downloading, and animating between the loading screen and showing the 3D product.
info
Looking for the official docs for customizing the Dopple loading screen? Click here for the Visual Component or Visual API docs.
The final loading screen
Here’s a quick preview of what we’ll be building:
Quick tip
You can resize that loading screen preview by dragging the corner to see what it will look like at different sizes.
Code setup
To get started, the general flow of building the custom loading screen will be:
- Create a container element for Dopple and the loading screen
- Overlay the loading screen on top of Dopple
- Hide Dopple by default when the page first loads
- Listen for Dopple to finish loading, then hide the loading screen and show the 3D product
In your page’s HTML, this will look roughly like:
<div class="container">
<!-- Dopple's canvas (or an <atlatl-visual> element instead, if using the Visual Component) -->
<canvas id="dopple-visual"></canvas>
<!-- Your custom loading screen -->
<div id="loading-screen"></div>
</div>
Heads up
The examples in this tutorial will use the Visual Component (with the <atlatl-visual>
element), but the same steps apply if you’re using the Visual API (with a <canvas>
element) instead. We’ll call out any differences along the way.
Sizing and positioning each element
To keep things simple, we’ll only manually size the container to look how we want on our page, then have Dopple and the loading screen both take up the full width and height of that container.
/* Feel free to size and style your container however you like! */
.container {
height: 480px;
width: 640px;
}
/* The containers contents will adjust to match its size */
#loading-screen,
#dopple-visual {
height: 100%;
width: 100%;
}
To position the loading screen on top of your 3D product, we’ll use a handy trick with CSS grid-template-areas
on the container and grid-area
on each child to assign both Dopple and the loading screen to the same grid cell.
.container {
display: grid;
/* Create just a single cell in the grid named 'loader-content' */
grid-template-areas: "loader-content";
height: 480px;
overflow: hidden;
width: 640px;
}
#loading-screen,
#dopple-visual {
/* Have both elements take up the same grid cell */
grid-area: loader-content;
height: 100%;
width: 100%;
}
Building the loading screen
For the loading screen itself, we’ll use a simple SVG spinner, some “Loading...” text, and a small image. Then, using the same grid positioning trick above, we can stack the spinner and text on top of each other and center them both up.
Don’t forget: your loading screen <div>
will have a transparent background by default, so be sure to set a background color or gradient on it, or give it a background image with background-size: cover;
to make sure nothing behind it is visible until it’s ready!
<div id="loading-screen">
<svg class="spinner"></svg>
<div class="loading-text">
<img src="./path/to/your/image.png" alt="" />
Loading...
</div>
</div>
#loading-screen {
background-image: url("./path/to/your/background-image.jpg");
background-size: cover;
background-position: center center;
color: #FFF;
display: grid;
grid-template-areas: "loader-content";
height: 100%;
place-content: center;
user-select: none;
width: 100%;
}
.spinner,
.loading-text {
grid-area: loader-content;
}
To create the spinner, we’ll use two <circle>
elements — one for the full, semi-transparent ring, and one for the shorter opaque segment.
Tip
On the segment’s <circle>
, setting the pathLength
attribute to a round number like 100
will make it easy to set its stroke dash later on.
<!-- Create a 200x200 SVG -->
<svg viewBox="0 0 200 200" class="spinner">
<!-- Center up each circle at (100, 100) within the SVG -->
<!-- A radius of 98 will give just enough room for a 4pt stroke to not overflow the viewBox -->
<circle class="spinner-ring" cx="100" cy="100" r="98" />
<circle class="spinner-segment" cx="100" cy="100" r="98" pathLength="100" />
</svg>
.spinner {
display: block;
max-width: 100%;
width: 200px;
}
.spinner circle {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-width: 4;
transform-origin: 50% 50%;
}
.spinner-ring {
opacity: 0.25;
}
.spinner-segment {
animation: rotate 1.25s linear infinite;
/* Since we set pathLength to 100, it's easy to set the stroke length to be 15% of the circle,
* and the remaining 85% will be a gap in the stroke */
stroke-dasharray: 15, 85;
stroke-width: 1;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
And finally, for the “Loading...” text and image, we’ll center them up and add a little bit of spacing between them with the help of flexbox.
.loading-text {
align-items: center;
display: flex;
flex-direction: column;
font-size: 0.875rem;
gap: 1rem;
justify-content: center;
}
.loading-text img {
max-width: 100%;
width: 3rem;
}
Hiding Dopple before it loads
By default, Dopple will display a loading screen while the 3D assets are being downloaded, but if using the Visual API then this screen can be hidden when you first call initClient()
using the showLoadingScreen
option:
await visual.initClient('a1a1a1a1-b2b2-c3c3-d4d4-e5e5e5e5e5e5', {
showLoadingScreen: false // 'true' by default
})
As an added precaution, or if using the Visual Component instead of the Visual API, we can also style Dopple to be hidden by default until everything is done loading.
#dopple-visual {
opacity: 0;
visibility: hidden;
}
Hiding the loading screen once the product is ready
For the final step, we’ll listen for when all of the 3D assets are finished loading and the 3D product is ready to be displayed using Dopple’s await product.ready()
method.
When that resolves, we simply apply a done-loading
class to the container to fade out the loading screen and show the 3D Dopple product.
#loading-screen {
transition: all 1s cubic-bezier(0, 0.5, 0, 1);
}
/* Hide the loading screen element */
.container.done-loading #loading-screen {
opacity: 0;
visibility: hidden;
}
/* Show the 3D Dopple product */
.container.done-loading #dopple-visual {
opacity: 1;
visibility: visible;
}
// Wait for the 3D product to finish loading
await product.ready()
// Apply the `done-loading` class to the container to fade out the loading screen
const doppleContainer = document.querySelector('.container')
doppleContainer.classList.add('done-loading')
Heads up
If using the Visual Component, you can listen for ready()
on the <atlatl-visual>
element directly:
const doppleContainer = document.querySelector('.container')
const doppleVisual = document.querySelector('atlatl-visual')
window.addEventListener('load', async () => {
await doppleVisual.ready()
doppleContainer.classList.add('done-loading')
})
And voilà — you’ve got yourself a snazzy new loading screen for your 3D experience!
From here, it’s easy to add even more effects like a scale transform to the loading screen as it fades out, or an animation to spinner’s segment length as it spins. Feel free to get creative with it and customize it any way you like!
Full code example (Visual API)
- HTML
- CSS
- JS
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Page</title>
<!-- Link to the Dopple's Visual API scripts -->
<script src="https://builds.dopple.io/atlatl-visual-api/releases/current/index.js" defer></script>
<!-- Link to style.css -->
<link rel="stylesheet" href="style.css">
<!-- Link to custom scripts -->
<script src="scripts.js" defer></script>
</head>
<body>
<div class="container">
<!-- The 3D Dopple product -->
<canvas id="dopple-visual"></canvas>
<!-- Custom loading screen -->
<div id="loading-screen">
<svg viewBox="0 0 200 200" class="spinner">
<circle class="spinner-ring" cx="100" cy="100" r="98" />
<circle class="spinner-segment" cx="100" cy="100" r="98" pathLength="100" />
</svg>
<div class="loading-text">
<img src="./path/to/your/image.png" alt="" />
Loading...
</div>
</div>
</div>
</body>
</html>
.container {
display: grid;
grid-template-areas: "loader-content";
height: 480px;
overflow: hidden;
width: 640px;
}
#dopple-visual,
#loading-screen {
grid-area: loader-content;
height: 100%;
width: 100%;
}
#dopple-visual {
opacity: 0;
visibility: hidden;
}
#loading-screen {
background-image: url("./path/to/your/background-image.jpg");
background-size: cover;
background-position: center center;
color: #FFF;
display: grid;
grid-template-areas: "loader-content";
height: 100%;
place-content: center;
transition: all 1s cubic-bezier(0, 0.5, 0, 1);
user-select: none;
width: 100%;
}
.container.done-loading #loading-screen {
opacity: 0;
visibility: hidden;
}
.container.done-loading #dopple-visual {
opacity: 1;
visibility: visible;
}
.spinner,
.loading-text {
grid-area: loader-content;
}
.spinner {
display: block;
max-width: 100%;
width: 200px;
}
.spinner circle {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-width: 4;
transform-origin: 50% 50%;
}
.spinner-ring {
opacity: 0.25;
}
.spinner-segment {
animation: rotate 1.25s linear infinite;
stroke-dasharray: 15, 85;
stroke-width: 1;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
align-items: center;
display: flex;
flex-direction: column;
font-size: 0.875rem;
gap: 1rem;
justify-content: center;
}
.loading-text img {
max-width: 100%;
width: 3rem;
}
let visual
let myProduct
let namespace = 'my-namespace'
let name = 'my-product-name'
// Initialize Dopple Visual and load the product once the document is ready
window.addEventListener('load', async () => {
const renderCanvas = document.getElementById('my-canvas')
visual = new Atlatl.Visual(renderCanvas)
// Replace with your client ID here
await visual.initClient('a1a1a1a1-b2b2-c3c3-d4d4-e5e5e5e5e5e5'{
showLoadingScreen: false
})
// Create and load the ProductTemplate
const template = new Atlatl.ProductTemplate(visual, namespace, name)
await template.load()
// Create the Product instance
myProduct = new Atlatl.Product(template)
// Hide the loading screen to show the product once the product is ready
await myProduct.ready()
// Apply the `done-loading` class to the container to fade out the loading screen
const doppleContainer = document.querySelector('.container')
doppleContainer.classList.add('done-loading')
})
Full code example (Visual Component)
- HTML
- CSS
- JS
<html lang="en">
<head>
<meta charset="utf-8">
<title>My Page</title>
<!-- Link to the Dopple's Visual Component scripts -->
<script src="https://builds.dopple.io/atlatl-visual-component/releases/current/index.js" defer></script>
<!-- Link to style.css -->
<link rel="stylesheet" href="style.css">
<!-- Link to custom scripts -->
<script src="scripts.js" defer></script>
</head>
<body>
<div class="container">
<!-- The 3D Dopple product -->
<atlatl-visual client-id="a1a1a1a1-b2b2-c3c3-d4d4-e5e5e5e5e5e5">
<av-product namespace="my_namespace" name="my_product_name"></av-product>
</atlatl-visual>
<!-- Custom loading screen -->
<div id="loading-screen">
<svg viewBox="0 0 200 200" class="spinner">
<circle class="spinner-ring" cx="100" cy="100" r="98" />
<circle class="spinner-segment" cx="100" cy="100" r="98" pathLength="100" />
</svg>
<div class="loading-text">
<img src="./path/to/your/image.png" alt="" />
Loading...
</div>
</div>
</div>
</body>
</html>
.container {
display: grid;
grid-template-areas: "loader-content";
height: 480px;
overflow: hidden;
width: 640px;
}
#dopple-visual,
#loading-screen {
grid-area: loader-content;
height: 100%;
width: 100%;
}
#dopple-visual {
opacity: 0;
visibility: hidden;
}
#loading-screen {
background-image: url("./path/to/your/background-image.jpg");
background-size: cover;
background-position: center center;
color: #FFF;
display: grid;
grid-template-areas: "loader-content";
height: 100%;
place-content: center;
transition: all 1s cubic-bezier(0, 0.5, 0, 1);
user-select: none;
width: 100%;
}
.container.done-loading #loading-screen {
opacity: 0;
visibility: hidden;
}
.container.done-loading #dopple-visual {
opacity: 1;
visibility: visible;
}
.spinner,
.loading-text {
grid-area: loader-content;
}
.spinner {
display: block;
max-width: 100%;
width: 200px;
}
.spinner circle {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-width: 4;
transform-origin: 50% 50%;
}
.spinner-ring {
opacity: 0.25;
}
.spinner-segment {
animation: rotate 1.25s linear infinite;
stroke-dasharray: 15, 85;
stroke-width: 1;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
align-items: center;
display: flex;
flex-direction: column;
font-size: 0.875rem;
gap: 1rem;
justify-content: center;
}
.loading-text img {
max-width: 100%;
width: 3rem;
}
const doppleContainer = document.querySelector('.container')
const doppleVisual = document.querySelector('atlatl-visual')
window.addEventListener('load', async () => {
await doppleVisual.ready()
doppleContainer.classList.add('done-loading')
})