Skip to main content

Working with 2D Views

Introduction

While Mimeeq is known for its powerful 3D configurator, many scenarios benefit from 2D visualization approaches. Whether you're creating a custom product gallery, integrating with an existing design system, or optimizing for performance on specific devices, Mimeeq's API provides flexible options for working with 2D product views.

This guide explores how to create custom 2D visualization experiences with Mimeeq, covering both standalone 2D implementations and approaches that combine 2D and 3D visualization.

Common Use Cases

  • Creating a custom product gallery with your own UI components
  • Implementing a lightbox for detailed product examination
  • Building responsive view navigation for different devices
  • Integrating with third-party carousel or gallery libraries
  • Offering both 2D and 3D visualization options to users

Understanding Mimeeq's 2D Views System

Mimeeq's 2D visualization system uses a layer-based approach for product views. Each view is composed of one or more image layers that are combined to create the final visualization. Some important characteristics:

  • Each view can consist of anywhere from one single static image to many dynamic layers
  • The structure and composition of these views are defined by the product administrator
  • Layers can be either transparent PNGs that build upon each other or full images with backgrounds
  • Dynamic layers can change based on selected configuration options
  • The system manages transitions between configurations by preserving unchanged layers while fading in new layers for a smooth visual experience

When using Mimeeq's built-in view management with setCarouselView(), the system handles the loading, compositing, and transitions of these layers automatically.

Available 2D Visualization Methods

Mimeeq provides several methods for working with 2D product views:

Important: Configuration Code for 2D Views

A crucial aspect of working with Mimeeq's 2D views is that you must explicitly pass the current configuration code to functions like getProductCarouselImages and get2dViewImage. If you don't provide a configuration code, these functions will use the product's default configuration code, not the currently selected options.

The correct way to get and use the current configuration code is through Mimeeq's observer pattern:

// Subscribe to configuration code changes
const configSub = window.mimeeqApp.observers.product.configurationCode.subscribe(async (data) => {
const currentConfigCode = data.newValue;

// Use the current configuration code with image functions
const images = await window.mimeeqApp.actions.getProductCarouselImages(viewIds, currentConfigCode);

// Update your UI with the new images
updateGalleryWithImages(images);
});

// Keep the subscription active to automatically update when configuration changes
// Only unsubscribe when appropriate (component unmount, page navigation, etc.)
function cleanup() {
configSub.unsubscribe();
}

This reactive approach ensures your 2D views always reflect the current product configuration.

The simplest approach is to leverage Mimeeq's built-in view system while providing your own UI controls:

document.addEventListener('mimeeq-app-loaded', async () => {
// Reference to gallery container
const galleryContainer = document.getElementById('custom-gallery');
galleryContainer.innerHTML = '';

// Use observer to get product data
const productSub = window.mimeeqApp.observers.product.mainProductData.subscribe(async (data) => {
const productData = data.newValue;

// First check if product has 2D views
if (!productData || !Array.isArray(productData.viewIds) || productData.viewIds.length === 0) {
console.log('This product does not have 2D views');
return;
}

// Filter out 3D view if needed
const viewIds = productData.viewIds.filter(id => id !== '3d');

// Subscribe to configuration changes to keep images up to date
const configSub = window.mimeeqApp.observers.product.configurationCode.subscribe(async (data) => {
const configCode = data.newValue;

// Get image URLs for all views with the current configuration
const images = await window.mimeeqApp.actions.getProductCarouselImages(viewIds, configCode);

// Clear existing thumbnails
galleryContainer.innerHTML = '';

// Create thumbnail gallery
images.forEach(view => {
const thumbnail = document.createElement('div');
thumbnail.classList.add('thumbnail');
thumbnail.dataset.viewId = view.viewId;
thumbnail.innerHTML = `<img src="${view.image}" alt="Product view">`;

// Handle thumbnail click to change view
thumbnail.addEventListener('click', () => {
// Let Mimeeq handle view switching
window.mimeeqApp.actions.setCarouselView(view.viewId);

// Update selected state in UI
document.querySelectorAll('.thumbnail').forEach(el => el.classList.remove('active'));
thumbnail.classList.add('active');
});

galleryContainer.appendChild(thumbnail);
});

// Select first view initially
if (images.length > 0 && !document.querySelector('.thumbnail.active')) {
window.mimeeqApp.actions.setCarouselView(images[0].viewId);
galleryContainer.querySelector('.thumbnail').classList.add('active');
}
});

// Also subscribe to view changes to keep the UI in sync
const viewSub = window.mimeeqApp.observers.product.currentViewId.subscribe((data) => {
const currentViewId = data.newValue;

// Update selected state in UI
document.querySelectorAll('.thumbnail').forEach(el => {
el.classList.toggle('active', el.dataset.viewId === currentViewId);
});
});

// Clean up subscription when no longer needed
// For example, when the component is unmounted or destroyed
function cleanup() {
productSub.unsubscribe();
configSub.unsubscribe();
viewSub.unsubscribe();
}
});
});

With HTML structure:

<div class="product-viewer">
<!-- Mimeeq's canvas will render the main view -->
<div id="mmq-canvas"></div>

<!-- Custom gallery navigation -->
<div id="custom-gallery" class="thumbnail-gallery"></div>
</div>

Fully Custom 2D Implementation

For complete control over the 2D viewing experience, you can build a custom viewer that directly retrieves and displays images:

document.addEventListener('mimeeq-app-loaded', async () => {
// Create custom viewer components
const viewerContainer = document.getElementById('custom-2d-viewer');
const mainView = document.createElement('div');
mainView.className = 'main-view';

const thumbnailStrip = document.createElement('div');
thumbnailStrip.className = 'thumbnail-strip';

viewerContainer.appendChild(mainView);
viewerContainer.appendChild(thumbnailStrip);

// Track current view
let currentViewId = null;
let viewIds = [];

// Use observer to get product data
const productSub = window.mimeeqApp.observers.product.mainProductData.subscribe(async (data) => {
const productData = data.newValue;

// Check if product has 2D capability
if (!productData || !Array.isArray(productData.viewIds) || productData.viewIds.length === 0) {
viewerContainer.innerHTML = '<p>This product does not have 2D views</p>';
return;
}

// Filter out 3D view and store available view IDs
viewIds = productData.viewIds.filter(id => id !== '3d');

// Initialize current view ID if not set yet
if (!currentViewId && viewIds.length > 0) {
currentViewId = viewIds[0];
}

// Subscribe to configuration changes
const configSub = window.mimeeqApp.observers.product.configurationCode.subscribe(async (data) => {
const configCode = data.newValue;

// Function to set the main image
async function setMainImage(viewId) {
// Show loading state
mainView.innerHTML = '<div class="loading-spinner"></div>';

try {
// Get high-quality image for the main view with current configuration
const imageUrl = await window.mimeeqApp.actions.get2dViewImage(
viewId,
configCode,
'x1200' // High-resolution image
);

if (!imageUrl) {
mainView.innerHTML = '<p>Image not available</p>';
return;
}

// Create and display the image
const img = new Image();
img.onload = () => {
mainView.innerHTML = '';
mainView.appendChild(img);
};
img.onerror = () => {
mainView.innerHTML = '<p>Failed to load image</p>';
};
img.src = imageUrl;
img.alt = "Product view";
img.className = "main-product-image";
} catch (error) {
console.error('Error loading image:', error);
mainView.innerHTML = '<p>Error loading image</p>';
}

// Update active state in thumbnails
document.querySelectorAll('.thumb').forEach(el => {
el.classList.toggle('active', el.dataset.viewId === viewId);
});
}

// Also update thumbnails with current configuration
thumbnailStrip.innerHTML = '';

// Create thumbnails for all views with current configuration
for (const viewId of viewIds) {
// Get thumbnail image with current configuration
const thumbnailUrl = await window.mimeeqApp.actions.get2dViewImage(
viewId,
configCode, // Use current configuration
'x210' // Smaller size for thumbnails
);

const thumb = document.createElement('div');
thumb.className = 'thumb';
thumb.dataset.viewId = viewId;
thumb.innerHTML = `<img src="${thumbnailUrl}" alt="View thumbnail">`;

// Check if this is the current view
if (viewId === currentViewId) {
thumb.classList.add('active');
}

thumb.addEventListener('click', () => {
currentViewId = viewId;
setMainImage(viewId);
});

thumbnailStrip.appendChild(thumb);
}

// Update main image if we have a current view
if (currentViewId) {
setMainImage(currentViewId);
}
});

// Clean up subscription when no longer needed
function cleanup() {
productSub.unsubscribe();
configSub.unsubscribe();
}
});
});

With corresponding CSS:

#custom-2d-viewer {
width: 100%;
max-width: 800px;
margin: 0 auto;
}

.main-view {
width: 100%;
height: 500px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
margin-bottom: 20px;
position: relative; /* For positioning loading indicators */
}

.main-view img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: opacity 0.3s ease; /* Smooth transition between images */
}

.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.thumbnail-strip {
display: flex;
gap: 10px;
overflow-x: auto;
padding: 10px 0;
}

.thumb {
width: 60px;
height: 60px;
flex-shrink: 0;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}

.thumb.active {
border-color: #3366ff;
}

.thumb img {
width: 100%;
height: 100%;
object-fit: contain;
}

/* Responsive styles */
@media (max-width: 768px) {
.main-view {
height: 350px;
}

.thumb {
width: 50px;
height: 50px;
}
}

Integrating with Third-Party Libraries

document.addEventListener('mimeeq-app-loaded', async () => {
// Get product views
let productData;
window.mimeeqApp.observers.product.mainProductData.subscribe(({ newValue }) => {
productData = newValue;
})?.unsubscribe();

if (!productData || !Array.isArray(productData.viewIds) || productData.viewIds.length === 0) {
console.log('This product does not have 2D views');
return;
}

const viewIds = productData.viewIds.filter(id => id !== '3d');

// Create Swiper container
const swiperContainer = document.getElementById('product-swiper');

// Subscribe to configuration changes to keep images up to date
const configSub = window.mimeeqApp.observers.product.configurationCode.subscribe(async (data) => {
const configCode = data.newValue;

// Get images with current configuration
const images = await window.mimeeqApp.actions.getProductCarouselImages(viewIds, configCode);

// Create slides HTML
let slidesHtml = '';
images.forEach(view => {
slidesHtml += `
<div class="swiper-slide" data-view-id="${view.viewId}">
<img src="${view.image}" alt="Product view">
</div>
`;
});

// Create thumbs HTML
let thumbsHtml = '';
images.forEach(view => {
thumbsHtml += `
<div class="swiper-slide thumb-slide" data-view-id="${view.viewId}">
<img src="${view.image}" alt="Product thumbnail">
</div>
`;
});

// Set up HTML structure
swiperContainer.innerHTML = `
<div class="swiper main-swiper">
<div class="swiper-wrapper">${slidesHtml}</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
<div class="swiper thumbs-swiper">
<div class="swiper-wrapper">${thumbsHtml}</div>
</div>
`;

// Initialize Swiper (requires Swiper.js to be loaded)
const thumbsSwiper = new Swiper('.thumbs-swiper', {
slidesPerView: 5,
spaceBetween: 10,
watchSlidesProgress: true,
breakpoints: {
// When screen width is <= 768px
768: {
slidesPerView: 4,
},
// When screen width is <= 480px
480: {
slidesPerView: 3,
}
}
});

const mainSwiper = new Swiper('.main-swiper', {
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
thumbs: {
swiper: thumbsSwiper,
},
on: {
slideChange: function() {
const activeSlide = this.slides[this.activeIndex];
const viewId = activeSlide.dataset.viewId;

// Update Mimeeq's internal state to match the swiper
window.mimeeqApp.actions.setCarouselView(viewId);
}
}
});
});

// Clean up subscription when no longer needed
function cleanup() {
configSub.unsubscribe();
}

// Subscribe to view changes to keep the swiper in sync
const viewSub = window.mimeeqApp.observers.product.currentViewId.subscribe((data) => {
const viewId = data.newValue;

// Find Swiper instance if it exists
const mainSwiper = document.querySelector('.main-swiper')?.swiper;

if (mainSwiper) {
const slideIndex = Array.from(document.querySelectorAll('.main-swiper .swiper-slide'))
.findIndex(slide => slide.dataset.viewId === viewId);

if (slideIndex !== -1 && mainSwiper.activeIndex !== slideIndex) {
mainSwiper.slideTo(slideIndex);
}
}
});
});

Implementing a Lightbox with GLightbox

Mimeeq's built-in implementation uses GLightbox for displaying high-resolution product images. You can create your own custom implementation using the same library:

document.addEventListener('mimeeq-app-loaded', async () => {
// Check product data
let productData;
window.mimeeqApp.observers.product.mainProductData.subscribe(({ newValue }) => {
productData = newValue;
})?.unsubscribe();

if (!productData) {
console.log('Product data not available');
return;
}

// Container for gallery
const galleryContainer = document.getElementById('lightbox-gallery');

// Subscribe to configuration changes
const configSub = window.mimeeqApp.observers.product.configurationCode.subscribe(async (data) => {
const configCode = data.newValue;

// Get high-resolution images for lightbox with current configuration
// This also works without explicitly passing a configuration, as it will use the latest
const lightboxImages = await window.mimeeqApp.actions.getProductLightboxImages();

if (!lightboxImages || lightboxImages.length === 0) {
console.log('No lightbox images available');
galleryContainer.innerHTML = '<p>No images available</p>';
return;
}

// Clear existing gallery
galleryContainer.innerHTML = '';

// Create gallery items
lightboxImages.forEach((image, index) => {
const galleryItem = document.createElement('a');
galleryItem.href = image.original || image.large;
galleryItem.dataset.gallery = "product-gallery";
galleryItem.dataset.caption = `${productData.metadata.displayName} - View ${index + 1}`;

// Create thumbnail
const thumbnail = document.createElement('img');
thumbnail.src = image.large;
thumbnail.alt = "Product view";
thumbnail.classList.add('lightbox-thumbnail');

galleryItem.appendChild(thumbnail);
galleryContainer.appendChild(galleryItem);
});

// Initialize GLightbox
const lightbox = GLightbox({
selector: '[data-gallery="product-gallery"]',
touchNavigation: true,
loop: true,
autoplayVideos: true,
openEffect: 'fade',
closeEffect: 'fade',
cssEfects: {
fade: { in: 'fadeIn', out: 'fadeOut' }
}
});

// Add download button functionality
lightbox.on('slide_changed', ({ prev, current }) => {
const downloadBtn = document.querySelector('.glightbox-container .gdownload');
if (downloadBtn && current.slideNode && current.slideNode.querySelector('img')) {
const imgSrc = current.slideNode.querySelector('img').src;
downloadBtn.setAttribute('href', imgSrc);
downloadBtn.setAttribute('download', `${productData.metadata.name}-view-${current.index + 1}.jpg`);
}
});
});

// Clean up when no longer needed
function cleanup() {
configSub.unsubscribe();
}
});

With corresponding CSS:

.lightbox-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin: 20px 0;
}

.lightbox-thumbnail {
width: 100%;
height: auto;
object-fit: contain;
border: 1px solid #eee;
border-radius: 4px;
transition: transform 0.2s;
cursor: pointer;
}

.lightbox-thumbnail:hover {
transform: scale(1.05);
border-color: #3366ff;
}

/* Add responsive styles */
@media (max-width: 768px) {
.lightbox-gallery {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
}

Combining 2D and 3D Visualization

Some scenarios benefit from offering both 2D and 3D visualization options. Here's how to implement a toggle between these modes:

document.addEventListener('mimeeq-app-loaded', async () => {
// Reference to toggle button
const viewToggle = document.getElementById('view-toggle');

// Check if product data is available
let productData;
window.mimeeqApp.observers.product.mainProductData.subscribe(({newValue}) => {
productData = newValue;
})?.unsubscribe();

if (!productData) {
console.log('Product data not available');
return;
}

// Check if 3D is available for this product
const has3D = productData.metadata.type === '3D';

// Check if 2D views are available
const viewIds = (productData.viewIds || []).filter(id => id !== '3d');
const has2D = viewIds.length > 0;

// Show toggle only if both 2D and 3D are available
if (!has3D || !has2D) {
viewToggle.style.display = 'none';
return;
}

// Subscribe to view changes to keep UI in sync
const viewSub = window.mimeeqApp.observers.product.currentViewId.subscribe((data) => {
const viewId = data.newValue;

// Update mode display and toggle button text
const is3DMode = viewId === '3d';
viewToggle.textContent = is3DMode ? 'Switch to 2D' : 'Switch to 3D';

// You could also update other UI elements based on mode
document.documentElement.dataset.viewMode = is3DMode ? '3d' : '2d';
});

// Handle toggle click
viewToggle.addEventListener('click', () => {
// Get current view ID
window.mimeeqApp.observers.product.currentViewId.subscribe(({newValue}) => {
const currentViewId = newValue;

const is3DMode = currentViewId === '3d';

if (is3DMode) {
// Switch to 2D - use first available 2D view
if (viewIds.length > 0) {
window.mimeeqApp.actions.setCarouselView(viewIds[0]);
}
} else {
// Switch to 3D
window.mimeeqApp.actions.setCarouselView('3d');
}
})?.unsubscribe();
});

// Set initial mode - default to 3D if available
window.mimeeqApp.actions.setCarouselView('3d');
});

Layer Management and Transitions

Mimeeq's 2D system handles layers with sophisticated transition effects. When configuration changes, the system:

  1. Preserves layers that remain unchanged between configurations
  2. Applies fade-in animations to new layers
  3. Manages loading states for each layer individually
  4. Creates smooth transitions between configurations

Best Practices

Performance Considerations

  • Use Mimeeq's view system when possible: For optimal performance, let Mimeeq handle view switching with setCarouselView rather than building a completely custom solution. The built-in system efficiently manages layer loading, caching, and transitions.

  • Check product capabilities first: Always verify that a product has 2D views available before attempting to implement a 2D gallery.

  • Use appropriate image sizes: Request larger images only when needed for main views or lightboxes; use smaller thumbnails for galleries.

  • Provide loading states: Always show loading indicators when fetching new images to improve the user experience.

  • Maintain active subscriptions: Keep observer subscriptions active throughout the component lifecycle to ensure views stay in sync with configuration changes.

Accessibility

Ensure your custom 2D views are accessible:

  • Add proper alt text to all images
  • Ensure keyboard navigation works for galleries
  • Use ARIA attributes for custom controls
  • Maintain sufficient color contrast for UI elements
// Example of accessible thumbnail creation
function createAccessibleThumbnail(view, index) {
const thumb = document.createElement('button');
thumb.className = 'thumb';
thumb.dataset.viewId = view.viewId;
thumb.setAttribute('aria-label', `View ${index + 1}`);
thumb.setAttribute('role', 'tab');
thumb.setAttribute('aria-selected', index === 0 ? 'true' : 'false');

const img = document.createElement('img');
img.src = view.image;
img.alt = ""; // Empty alt since the button already has a label

thumb.appendChild(img);

return thumb;
}

Conclusion

Custom 2D visualization in Mimeeq offers flexibility while allowing you to maintain a consistent user experience. By leveraging Mimeeq's API methods for image retrieval and view management, you can create tailored 2D product displays that meet your specific requirements, whether as standalone implementations or in conjunction with 3D visualization.

When implementing custom 2D views, focus on creating smooth, responsive experiences that adapt to different devices and screen sizes. Whenever possible, work with Mimeeq's built-in view management system for optimal performance, and remember to always pass the current configuration code to image functions to ensure your views reflect the current product state.

For more complex implementations, consider using established third-party libraries for carousels and lightboxes, which can be easily integrated with Mimeeq's image generation methods, or leverage Mimeeq's built-in components for a consistent experience.