Web component for easily displaying interactive 3D models with AR support across browsers and devices
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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;
}