or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

accessibility.mdanimation.mdannotation.mdar.mdcontrols.mdenvironment.mdindex.mdloading.mdscene-graph.md
tile.json

annotation.mddocs/

Hotspots and Annotations

Add interactive hotspots and annotations positioned in 3D space to provide contextual information and enhanced user interaction with models.

Capabilities

Hotspot Management

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;
}

HTML Hotspot Creation

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 });
  }
});

Dynamic Hotspot Creation

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'
);

Hotspot Visibility and Interaction

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);

Interactive Hotspot Behaviors

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);
});

Styling Hotspots

CSS Styling Examples

/* 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;
  }
}

Themed Hotspot Variants

/* 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;
}