Add interactive hotspots and annotations positioned in 3D space to provide contextual information and enhanced user interaction with models.
Query and manipulate hotspot elements within the model viewer.
/**
* Find a hotspot element by its name/slot attribute
* @param name - The slot name of the hotspot element
* @returns The hotspot HTML element or null if not found
*/
queryHotspot(name: string): HTMLElement | null;
/**
* Position a hotspot element at a specific 3D coordinate
* @param hotspot - The hotspot HTML element to position
* @param position - 3D world coordinates for positioning
*/
positionHotspot(hotspot: HTMLElement, position: Vector3D): void;
/**
* Update hotspot configuration and properties
* @param config - Hotspot configuration object
*/
updateHotspot(config: HotspotConfiguration): void;
interface Vector3D {
x: number;
y: number;
z: number;
toString(): string;
}
interface HotspotConfiguration {
name: string;
position: Vector3D;
normal?: Vector3D;
visible?: boolean;
}Create hotspots using standard HTML elements with special slot attributes.
Basic Hotspot Structure:
<model-viewer src="models/product.glb">
<!-- Basic text hotspot -->
<div slot="hotspot-info" class="hotspot">
<div class="hotspot-content">
<h3>Key Feature</h3>
<p>This component provides enhanced durability.</p>
</div>
</div>
<!-- Interactive button hotspot -->
<button slot="hotspot-button" class="action-hotspot" onclick="showDetails()">
Learn More
</button>
<!-- Rich content hotspot -->
<div slot="hotspot-specs" class="rich-hotspot">
<img src="icons/info.svg" alt="Info">
<div class="popup">
<h4>Technical Specifications</h4>
<ul>
<li>Material: Aluminum Alloy</li>
<li>Weight: 2.3kg</li>
<li>Dimensions: 30×20×15cm</li>
</ul>
</div>
</div>
</model-viewer>Usage:
// Position hotspots after model loads
modelViewer.addEventListener('load', () => {
// Position info hotspot on the front face
const infoHotspot = modelViewer.queryHotspot('info');
if (infoHotspot) {
modelViewer.positionHotspot(infoHotspot, { x: 0, y: 1, z: 2 });
}
// Position button hotspot on the side
const buttonHotspot = modelViewer.queryHotspot('button');
if (buttonHotspot) {
modelViewer.positionHotspot(buttonHotspot, { x: 1.5, y: 0.5, z: 0 });
}
});Create and manage hotspots programmatically.
class HotspotManager {
constructor(modelViewer) {
this.modelViewer = modelViewer;
this.hotspots = new Map();
this.nextId = 1;
}
createHotspot(position, content, className = 'hotspot') {
const id = `hotspot-${this.nextId++}`;
// Create hotspot element
const hotspot = document.createElement('div');
hotspot.slot = id;
hotspot.className = className;
hotspot.innerHTML = content;
// Add to model viewer
this.modelViewer.appendChild(hotspot);
// Position in 3D space
this.modelViewer.positionHotspot(hotspot, position);
// Store reference
this.hotspots.set(id, {
element: hotspot,
position: position
});
return id;
}
removeHotspot(id) {
const hotspot = this.hotspots.get(id);
if (hotspot) {
hotspot.element.remove();
this.hotspots.delete(id);
}
}
updateHotspotPosition(id, newPosition) {
const hotspot = this.hotspots.get(id);
if (hotspot) {
this.modelViewer.positionHotspot(hotspot.element, newPosition);
hotspot.position = newPosition;
}
}
showAllHotspots() {
this.hotspots.forEach(hotspot => {
hotspot.element.style.display = 'block';
});
}
hideAllHotspots() {
this.hotspots.forEach(hotspot => {
hotspot.element.style.display = 'none';
});
}
}
// Usage
const hotspotManager = new HotspotManager(modelViewer);
// Create feature description hotspot
const featureId = hotspotManager.createHotspot(
{ x: 1, y: 2, z: 0.5 },
`<div class="feature-info">
<h4>Advanced Sensor</h4>
<p>High-precision environmental monitoring</p>
</div>`,
'feature-hotspot'
);
// Create warning hotspot
const warningId = hotspotManager.createHotspot(
{ x: -1, y: 0, z: 1 },
`<div class="warning">
<span class="icon">⚠️</span>
<span>High voltage area</span>
</div>`,
'warning-hotspot'
);Control hotspot visibility and interaction states.
// Hotspot visibility based on camera angle
function updateHotspotVisibility() {
const cameraOrbit = modelViewer.getCameraOrbit();
const theta = cameraOrbit.theta;
// Show front hotspots when viewing from front
const frontHotspots = ['info', 'features'];
frontHotspots.forEach(name => {
const hotspot = modelViewer.queryHotspot(name);
if (hotspot) {
const isFrontView = Math.abs(theta) < Math.PI / 2;
hotspot.style.display = isFrontView ? 'block' : 'none';
}
});
// Show side hotspots when viewing from side
const sideHotspots = ['controls', 'ports'];
sideHotspots.forEach(name => {
const hotspot = modelViewer.queryHotspot(name);
if (hotspot) {
const isSideView = Math.abs(theta) > Math.PI / 4;
hotspot.style.display = isSideView ? 'block' : 'none';
}
});
}
// Update visibility on camera changes
modelViewer.addEventListener('camera-change', updateHotspotVisibility);Create sophisticated interactive behaviors for hotspots.
class InteractiveHotspot {
constructor(modelViewer, config) {
this.modelViewer = modelViewer;
this.config = config;
this.element = this.createHotspotElement();
this.setupInteractions();
}
createHotspotElement() {
const hotspot = document.createElement('div');
hotspot.slot = this.config.name;
hotspot.className = 'interactive-hotspot';
hotspot.innerHTML = `
<div class="hotspot-trigger">
<div class="pulse-ring"></div>
<div class="hotspot-icon">${this.config.icon}</div>
</div>
<div class="hotspot-popup" style="display: none;">
<div class="popup-header">
<h4>${this.config.title}</h4>
<button class="close-btn">×</button>
</div>
<div class="popup-content">
${this.config.content}
</div>
</div>
`;
this.modelViewer.appendChild(hotspot);
this.modelViewer.positionHotspot(hotspot, this.config.position);
return hotspot;
}
setupInteractions() {
const trigger = this.element.querySelector('.hotspot-trigger');
const popup = this.element.querySelector('.hotspot-popup');
const closeBtn = this.element.querySelector('.close-btn');
// Show popup on click/tap
trigger.addEventListener('click', (e) => {
e.stopPropagation();
this.showPopup();
});
// Close popup
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.hidePopup();
});
// Hide on model click
this.modelViewer.addEventListener('click', () => {
this.hidePopup();
});
// Hover effects (desktop only)
if (!('ontouchstart' in window)) {
trigger.addEventListener('mouseenter', () => {
trigger.classList.add('hover');
});
trigger.addEventListener('mouseleave', () => {
trigger.classList.remove('hover');
});
}
}
showPopup() {
const popup = this.element.querySelector('.hotspot-popup');
popup.style.display = 'block';
this.element.classList.add('active');
// Position popup to avoid screen edges
this.positionPopup();
}
hidePopup() {
const popup = this.element.querySelector('.hotspot-popup');
popup.style.display = 'none';
this.element.classList.remove('active');
}
positionPopup() {
const popup = this.element.querySelector('.hotspot-popup');
const rect = this.element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Position popup to stay within viewport
if (rect.right + 300 > viewportWidth) {
popup.style.right = '100%';
popup.style.left = 'auto';
} else {
popup.style.left = '100%';
popup.style.right = 'auto';
}
if (rect.bottom + 200 > viewportHeight) {
popup.style.bottom = '0';
popup.style.top = 'auto';
}
}
}
// Usage - create interactive hotspots
const hotspots = [
{
name: 'engine',
position: { x: 0, y: 1, z: 2 },
icon: '⚙️',
title: 'Engine Specifications',
content: `
<p><strong>Type:</strong> V8 Turbocharged</p>
<p><strong>Power:</strong> 450 HP</p>
<p><strong>Torque:</strong> 520 Nm</p>
`
},
{
name: 'interior',
position: { x: 0, y: 0.5, z: 0 },
icon: '🪑',
title: 'Interior Features',
content: `
<ul>
<li>Premium leather seats</li>
<li>Climate control</li>
<li>Advanced infotainment</li>
</ul>
`
}
];
hotspots.forEach(config => {
new InteractiveHotspot(modelViewer, config);
});/* Basic hotspot styling */
.hotspot {
position: absolute;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
max-width: 200px;
font-size: 14px;
z-index: 100;
}
/* Hotspot trigger with pulse animation */
.hotspot-trigger {
position: relative;
width: 24px;
height: 24px;
cursor: pointer;
}
.pulse-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border: 2px solid #007aff;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(0.5);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(1.2);
}
}
/* Hotspot popup */
.hotspot-popup {
position: absolute;
top: 0;
left: 100%;
margin-left: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
max-width: 300px;
z-index: 1000;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
}
.popup-content {
padding: 16px;
}
/* Responsive hotspot styling */
@media (max-width: 768px) {
.hotspot-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 90vw;
max-height: 70vh;
overflow-y: auto;
}
}/* Warning hotspot */
.warning-hotspot {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
/* Success/feature hotspot */
.feature-hotspot {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
/* Info hotspot */
.info-hotspot {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
/* Minimalist hotspot */
.minimal-hotspot {
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 20px;
padding: 8px 16px;
font-size: 12px;
}