Loading Screens
Dopple utilizes three.js’s LoadingManager
under the hood to handle loading progress and completion, and is accessible via the loadingManager
property on your dopple
instance.
By default, no loading screen is shown while the product loads in.
const dopple = new DoppleXR({ /* ... */ });
dopple.loadingManager.onProgress = (_url, current, total) => { console.log(`Loading: ${current} of ${total}`);};
dopple.loadingManager.onLoad = () => { console.log("Product asset loaded!");};
dopple.loadingManager.onError = (url) => { console.error(`There was an error while loading: ${url}`);};
await dopple.load();console.log("Everything has finished loading!");
Basic Loading Screen
Section titled “Basic Loading Screen”Typically, for a good user experience, you’ll want to show a loading screen while the product loads in. One simple way to do this is to wrap both the Dopple canvas and the loading screen in a container element, with the loading screen placed above the product.
<div class="wrapper"> <div id="dopple-container"></div> <div class="loading-screen"> Loading... </div></div>
.wrapper { display: grid; height: 600px; width: 800px;}.wrapper > * { grid-area: 1 / 1;}#dopple-container { height: 100%; width: 100%;}.loading-screen { align-items: center; background: #FFF; display: flex; justify-content: center;}
Showing the Load Progress
Section titled “Showing the Load Progress”The onProgress
handler may be used to show the product’s loading progress in your UI, which is especially useful when the total file size of the product is large or when users may be on slow connections.
<div class="loading-screen"> Loading... <span class="progress">0%</span></div>
const progressElement = document.querySelector(".progress");
dopple.loadingManager.onProgress = (_url, current, total) => { progressElement.textContent = `${Math.round(current / total * 100)}%`;};
Hiding the Loading Screen
Section titled “Hiding the Loading Screen”To hide the loading screen, simply await
for dopple.load()
to resolve to signal that the entire product has finished loading, then hide or remove the loading screen element.
// Begin loading the productawait dopple.load();
// Hide the loading screen after the product has finished loadingconst loadingScreen = document.querySelector(".loading-screen");loadingScreen.style.display = "none";
Dopple Loading Screens
Section titled “Dopple Loading Screens”Looking for a flashier loading screen to display? Feel free to grab the code for one of our loaders below!
<div class="dopple-loading-screen"> <div class="cube"> <svg class="cube__face--top" viewBox="0 0 24 24"> <rect x="2" y="2" width="20" height="20" rx="2" /> </svg> <svg class="cube__face--front" viewBox="0 0 24 24"> <rect x="2" y="2" width="20" height="20" rx="2" /> </svg> <svg class="cube__face--left" viewBox="0 0 24 24"> <rect x="2" y="2" width="20" height="20" rx="2" /> </svg> <svg class="cube__face--right" viewBox="0 0 24 24"> <rect x="2" y="2" width="20" height="20" rx="2" /> </svg> <svg class="cube__face--back" viewBox="0 0 24 24"> <rect x="2" y="2" width="20" height="20" rx="2" /> </svg> </div> <span class="loading-text">Loading 3D...</span></div>
.dopple-loading-screen { align-items: center; background: #FFF; color: #1C1E21; display: flex; flex-direction: column; height: 100%; justify-content: center; padding-top: 1rem; perspective: 32rem;}58 collapsed lines
.cube { --size: 4; /* Control how big the cube is */ animation: 3s linear infinite cube-spin; height: calc(var(--size) * 1rem); position: relative; transform: rotateX(-40deg) rotateY(60deg); transform-style: preserve-3d; width: calc(var(--size) * 1rem);}[class^='cube__face--'] { -webkit-backface-visibility: hidden; backface-visibility: hidden; fill: none; height: 100%; left: 0; margin: 0 !important; position: absolute; stroke: #404755; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; top: 0; transform-origin: top; transform-style: preserve-3d; width: 100%;}@keyframes cube-spin { 0% { transform: rotateX(-40deg) rotateY(45deg); } to { transform: rotateX(-40deg) rotateY(-315deg); }}.cube__face--top { transform: translate3d(0, 0, calc(var(--size) * 1rem / -2)) rotateX(90deg);}.cube__face--front { transform: translateZ(calc(var(--size) * 1rem / 2)) rotateY(0);}.cube__face--left { transform: translateX(calc(var(--size) * 1rem / -2)) rotateY(-90deg);}.cube__face--right { transform: translateX(calc(var(--size) * 1rem / 2)) rotateY(90deg);}.cube__face--back { transform: translateZ(calc(var(--size) * 1rem / -2)) rotateY(180deg);}.loading-text { margin-top: 2.5rem;}@supports (container-type: inline-size) { .dopple-loading-screen { container-type: inline-size; } @container (max-width: 32rem) { .cube { --size: 2; } }}
<div class="dopple-loading-screen"> <div class="cube"> <div class="cube__face--top"></div> <div class="cube__face--front"></div> <div class="cube__face--back"></div> <div class="cube__face--left"></div> <div class="cube__face--right"></div> </div> <span class="loading-text">Loading...</span></div>
.dopple-loading-screen { --cube-size: 3rem; --duration: 3s; align-items: center; color: #1C1E21; background: #FFF; border-radius: 0.5rem; display: flex; flex-direction: column; gap: calc(var(--cube-size) + 1rem); height: 100%; justify-content: center; perspective: 48rem;154 collapsed lines
}.dopple-loading-screen * { margin: 0 !important;}.cube { animation: anim-cube-rotate var(--duration) linear infinite; height: var(--cube-size); position: relative; transform-style: preserve-3d; width: var(--cube-size);}[class^="cube__face--"],.cube__face--right::after { box-shadow: 0 0 0 0.5px #004586; content: ""; display: flex; height: 100%; left: 0; position: absolute; top: 0; transform-origin: top; transform-style: preserve-3d; width: 100%;}.cube__face--top { background: #005aaf; transform: rotateX(90deg) translate3d(0, calc(var(--cube-size) / -2), 0);}.cube__face--front { animation: anim-face--front var(--duration) ease infinite;}.cube__face--back { animation: anim-face--back var(--duration) ease infinite;}.cube__face--left { animation: anim-face--left var(--duration) ease infinite;}.cube__face--right { animation: anim-face--right var(--duration) ease infinite;}.cube__face--right::after { animation: anim-face--right--after var(--duration) ease infinite; transform-origin: bottom;}@keyframes anim-cube-rotate { from { transform: rotateX(-40deg) rotateY(0) translateY(0); } to { transform: rotateX(-40deg) rotateY(360deg) translateY(var(--cube-size)); }}@keyframes anim-face--front { from { background: #0052a0; transform: translateZ(calc(var(--cube-size) / 2)) rotateY(0deg); } 25% { background: #004586; transform: translateZ(calc(var(--cube-size) / 2)) rotateY(0deg) rotateX(180deg); } 50% { background: #00386d; } 75% { background: #004586; } to { background: #0052a0; transform: translateZ(calc(var(--cube-size) / 2)) rotateY(0deg) rotateX(180deg); }}@keyframes anim-face--left { from { background: #004586; transform: translateX(calc(var(--cube-size) / -2)) rotateY(-90deg); } 25% { background: #0052a0; transform: translateX(calc(var(--cube-size) / -2)) rotateY(-90deg); } 50% { background: #004586; transform: translateX(calc(var(--cube-size) / -2)) rotateY(-90deg) rotateX(180deg); } 75% { background: #00386d; } to { background: #004586; transform: translateX(calc(var(--cube-size) / -2)) rotateY(-90deg) rotateX(180deg); }}@keyframes anim-face--back { from { background: #00386d; transform: translateZ(calc(var(--cube-size) / -2)) rotateY(180deg); } 25% { background: #004586; } 50% { background: #0052a0; transform: translateZ(calc(var(--cube-size) / -2)) rotateY(180deg); } 75% { background: #004586; transform: translateZ(calc(var(--cube-size) / -2)) rotateY(180deg) rotateX(180deg); } to { background: #00386d; transform: translateZ(calc(var(--cube-size) / -2)) rotateY(180deg) rotateX(180deg); }}@keyframes anim-face--right { from { background: #004586; transform: translateX(calc(var(--cube-size) / 2)) rotateY(-90deg); } 25% { background: #00386d; } 50% { background: #004586; } 75% { background: #0052a0; transform: translateX(calc(var(--cube-size) / 2)) rotateY(-90deg); } to { background: #004586; transform: translateX(calc(var(--cube-size) / 2)) rotateY(-90deg) rotateX(-180deg); }}@keyframes anim-face--right--after { from { background: #00305d; transform: rotateX(-90deg); } 25% { background: #00305d; } 50% { background: #00305d; } 75% { background: #00305d; transform: rotateX(-90deg); } to { background: #005aaf; transform: rotateX(-270deg); }}
<div class="dopple-loading-screen"> <div class="dots"> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="lines"> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> <div class="line"></div> </div> </div> <span class="loading-text">Loading 3D...</span></div>
.dopple-loading-screen { align-items: center; background: #FFF; color: #1C1E21; display: flex; flex-direction: column; height: 100%; justify-content: center; left: 0; padding-top: 1.25rem; position: relative;193 collapsed lines
top: 0; width: 100%; z-index: 2;}.dopple-loading-screen * { margin: 0 !important;}.loading-text { margin-top: 2.5rem;}.dots { align-items: center; display: grid; height: 90px; justify-content: center; position: relative; width: 90px;}.dot,.line { background-color: #404755; grid-area: 1 / 1 / 2 / 2;}.dot,.line { animation-direction: alternate; animation-duration: 1.5s; animation-fill-mode: both; animation-iteration-count: infinite; animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); content: ''; position: relative;}.lines { align-items: center; content: ''; display: grid; grid-area: 1 / 1 / 2 / 2; height: 45px; justify-content: center; left: 50%; position: absolute; top: 50%; transform: translate(-50%, -50%); width: 3px;}.dot { animation-name: dot-1; border-radius: 9px; height: 9px; width: 9px;}.line { animation-name: line-1; height: 45px; position: relative; transform: translate(0); width: 3px;}@keyframes dot-1 { 0% { transform: translate(0); } 33% { transform: translate(-22.5px, -4.5px); } 67% { transform: translate(-9px, -18px); } to { transform: translate(-9px, -40.5px); }}@keyframes dot-2 { 0% { opacity: 0; transform: translate(0); } 33% { opacity: 1; transform: translate(-22.5px, -4.5px); } 67% { opacity: 1; transform: translate(-36px, 9px); } to { opacity: 1; transform: translate(-36px, -13.5px); }}@keyframes dot-3 { 0% { opacity: 0; transform: translate(0); } 33% { opacity: 0; transform: translate(-22.5px, -4.5px); } 67% { opacity: 1; transform: translate(-9px, -18px); } to { opacity: 1; transform: translate(-9px, 4.5px); }}@keyframes dot-4 { 0% { opacity: 0; transform: translate(0); } 33% { opacity: 0; transform: translate(-22.5px, -4.5px); } 67% { opacity: 1; transform: translate(-36px, 9px); } to { opacity: 1; transform: translate(-36px, 31.5px); }}@keyframes dot-5 { 0% { transform: translate(0); } 33% { transform: translate(22.5px, 4.5px); } 67% { transform: translate(9px, 18px); } to { transform: translate(9px, 40.5px); }}@keyframes dot-6 { 0% { opacity: 0; transform: translate(0); } 33% { opacity: 1; transform: translate(22.5px, 4.5px); } 67% { opacity: 1; transform: translate(36px, -9px); } to { opacity: 1; transform: translate(36px, 13.5px); }}@keyframes dot-7 { 0% { opacity: 0; transform: translate(0); } 33% { opacity: 0; transform: translate(22.5px, 4.5px); } 67% { opacity: 1; transform: translate(9px, 18px); } to { opacity: 1; transform: translate(9px, -4.5px); }}@keyframes dot-8 { 0% { opacity: 0; transform: translate(0); } 33% { opacity: 0; transform: translate(22.5px, 4.5px); } 67% { opacity: 1; transform: translate(36px, -9px); } to { opacity: 1; transform: translate(36px, -31.5px); }}.dot:nth-of-type(2) { animation-name: dot-2; }.dot:nth-of-type(3) { animation-name: dot-3; }.dot:nth-of-type(4) { animation-name: dot-4; }.dot:nth-of-type(5) { animation-name: dot-5; }.dot:nth-of-type(6) { animation-name: dot-6; }.dot:nth-of-type(7) { animation-name: dot-7; }.dot:nth-of-type(8) { animation-name: dot-8; }@keyframes line-1 { 0% { transform: translate(0) scaleY(0); } 67% { transform: translate(36px, -9px) scaleY(0); } to { transform: translate(36px, -9px) scaleY(1); }}@keyframes line-2 { 0% { transform: translate(0) scaleY(0); } 67% { transform: translate(-36px, 9px) scaleY(0); } to { transform: translate(-36px, 9px) scaleY(1); }}@keyframes line-3 { 0%, 33% { transform: translate(-22.5px, -4.5px) rotate(45deg) scaleY(0); } 67% { transform: translate(-22.5px, -4.5px) rotate(45deg) scaleY(0.85); } to { transform: translate(-22.5px, -27px) rotate(45deg) scaleY(0.85); }}@keyframes line-4 { 0%, 33% { transform: translate(22.5px, 4.5px) rotate(45deg) scaleY(0); } 67% { transform: translate(22.5px, 4.5px) rotate(45deg) scaleY(0.85); } to { transform: translate(22.5px, 27px) rotate(45deg) scaleY(0.85); }}@keyframes line-5 { 0% { transform: translate(0) rotate(-78.7deg) scaleY(0); } 33% { transform: translate(0) rotate(-78.7deg); } 67% { transform: translate(13.5px, -13.5px) rotate(-78.7deg); } to { transform: translate(13.5px, -36px) rotate(-78.7deg); }}@keyframes line-6 { 0% { transform: translate(0) rotate(-78.7deg) scaleY(0); } 33% { transform: translate(0) rotate(-78.7deg); } 67% { transform: translate(-13.5px, 13.5px) rotate(-78.7deg); } to { transform: translate(-13.5px, 36px) rotate(-78.7deg); }}@keyframes line-7 { 0%, 67% { transform: translate(9px, 18px) scaleY(0); } to { transform: translate(9px, 18px) scaleY(1); }}@keyframes line-8 { 0%, 67% { transform: translate(-9px, -18px) scaleY(0); } to { transform: translate(-9px, -18px) scaleY(1); }}@keyframes line-9 { 0% { transform: translate(0) rotate(-78.7deg) scaleY(0); } 33% { transform: translate(0) rotate(-78.7deg); } 67% { transform: translate(13.5px, -13.5px) rotate(-78.7deg); } to { transform: translate(13.5px, 9px) rotate(-78.7deg); }}@keyframes line-10 { 0% { transform: translate(0) rotate(-78.7deg) scaleY(0); } 33% { transform: translate(0) rotate(-78.7deg); } 67% { transform: translate(-13.5px, 13.5px) rotate(-78.7deg); } to { transform: translate(-13.5px, -9px) rotate(-78.7deg); }}@keyframes line-11 { 0%, 33% { transform: translate(-22.5px, -4.5px) rotate(45deg) scaleY(0); } 67% { transform: translate(-22.5px, -4.5px) rotate(45deg) scaleY(0.85); } to { transform: translate(-22.5px, 18px) rotate(45deg) scaleY(0.85); }}@keyframes line-12 { 0%, 33% { transform: translate(22.5px, 4.5px) rotate(45deg) scaleY(0); } 67% { transform: translate(22.5px, 4.5px) rotate(45deg) scaleY(0.85); } to { transform: translate(22.5px, -18px) rotate(45deg) scaleY(0.85); }}.line:nth-of-type(2) { animation-name: line-2; }.line:nth-of-type(3) { animation-name: line-3; }.line:nth-of-type(4) { animation-name: line-4; }.line:nth-of-type(5) { animation-name: line-5; }.line:nth-of-type(6) { animation-name: line-6; }.line:nth-of-type(7) { animation-name: line-7; }.line:nth-of-type(8) { animation-name: line-8; }.line:nth-of-type(9) { animation-name: line-9; }.line:nth-of-type(10) { animation-name: line-10; }.line:nth-of-type(11) { animation-name: line-11; }.line:nth-of-type(12) { animation-name: line-12; }
Full Code Example
Section titled “Full Code Example”<html> <head> <script type="module" src="script.js"></script> <link rel="stylesheet" href="style.css"> </head> <body> <div class="wrapper"> <div id="dopple-container"></div> <div class="loading-screen"> Loading... <span class="progress">0%</span> </div> </div> </body></html>
.wrapper { display: grid; height: 48rem; width: 64rem;}.wrapper > * { grid-area: 1 / 1;}#dopple-container { height: 100%; width: 100%;}.loading-screen { align-items: center; background: #FFF; display: flex; justify-content: center;}
import { DoppleXR } from "https://builds.dopple.io/packages/dopple-sdk@latest/dopple-sdk.js";
const dopple = new DoppleXR({ container: document.getElementById("dopple-container"), owner: "my-org", workspace: "my-workspace", projectName: "My Project", productVersion: "1", selection: {}});
const progressElement = document.querySelector(".progress");
// Update the loading progress in the UIdopple.loadingManager.onProgress = (_url, current, total) => { progressElement.textContent = `${Math.round(current / total * 100)}%`;};
// Handle when an asset within the product is done loadingdopple.loadingManager.onLoad = () => { console.log("Product asset loaded!")};
// Handle errorsdopple.loadingManager.onError = (url) => { console.error(`There was an error while loading: ${url}`);};
// Begin loading the productawait dopple.load();
// Hide the loading screen once the product is done loadingconst loadingScreen = document.querySelector(".loading-screen");loadingScreen.style.display = "none";
dopple.resize({ trackParentSize: true });dopple.run();