Product 360° View Development for E-Commerce
360° view — sequence of photos from equal steps in circle. User drags left-right to see product from all sides. Technically not video or 3D — just animation through static frames, but creates interactive rotation illusion.
Photography for 360°
Result depends on shooting, not code. Technical requirements:
- Frame count: 24–72. 24 = 15° step (enough), 36 = 10° (smooth), 72 = 5° (very smooth, 3x data)
- Motorized turntable — key equipment for equal step
- Constant lighting — no moving shadows between frames
- Background: white or transparent (PNG alpha) to embed in any design
- Resolution: 1000–2000px — balance quality/weight
Typical volume: 36 frames × 200KB = 7MB per product. Much for page. Solved via progressive loading.
Frame Storage Formats
Three options:
Separate files: /360/product-42/frame-{01..36}.webp. Simple, transparent for CDN caching. On frame change browser loads file (or caches).
Sprite sheet: all frames in one image (6×6 grid for 36). Fewer HTTP requests, but one large file. Display via background-position. Good if all frames needed at once.
Video file: frames convert to MP4 no sound, play via <video> with currentTime control. Most compact (H.264 compression). Control:
const video = document.querySelector('video');
let isDragging = false;
canvas.addEventListener('mousedown', e => {
isDragging = true;
startX = e.clientX;
startTime = video.currentTime;
});
canvas.addEventListener('mousemove', e => {
if (!isDragging) return;
const delta = (e.clientX - startX) / canvas.offsetWidth;
video.currentTime = Math.max(0, Math.min(
video.duration,
startTime - delta * video.duration
));
});
Implementation on Separate Frames
Most common — image array:
class Product360Viewer {
private frames: HTMLImageElement[] = [];
private currentFrame = 0;
private isDragging = false;
constructor(
private container: HTMLElement,
private canvas: HTMLCanvasElement,
private frameUrls: string[]
) {
this.preloadFrames();
this.bindEvents();
}
private preloadFrames() {
const loadFrame = (index: number) => {
const img = new Image();
img.src = this.frameUrls[index];
img.onload = () => {
this.frames[index] = img;
if (index === 0) this.render(0);
if (index < this.frameUrls.length - 1) loadFrame(index + 1);
};
};
loadFrame(0);
}
private render(frameIndex: number) {
const ctx = this.canvas.getContext('2d')!;
const img = this.frames[frameIndex];
if (!img) return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height);
}
private handleDrag(deltaX: number) {
const sensitivity = 3; // pixels per frame
const frameDelta = Math.round((this.startX - deltaX) / sensitivity);
this.currentFrame = ((this.startFrame + frameDelta) % this.frameUrls.length + this.frameUrls.length) % this.frameUrls.length;
this.render(this.currentFrame);
}
}
Progressive Loading
7MB immediately on page open — unacceptable. Strategy:
- Display static image (first frame) — already in gallery
- On hover /
IntersectionObserver— start frame loading - Show "Loading 360°: 45%" indicator
- At >50% loaded — activate interactivity
- Continue loading rest in background
Odd-numbered frames first (0, 2, 4, 8, 16, 32...) — rough interactivity, then fill gaps.
Touch Events for Mobile
private bindEvents() {
// Mouse
this.canvas.addEventListener('mousedown', e => this.startDrag(e.clientX));
window.addEventListener('mousemove', e => { if (this.isDragging) this.handleDrag(e.clientX); });
window.addEventListener('mouseup', () => this.isDragging = false);
// Touch
this.canvas.addEventListener('touchstart', e => {
e.preventDefault();
this.startDrag(e.touches[0].clientX);
}, { passive: false });
this.canvas.addEventListener('touchmove', e => {
e.preventDefault();
if (this.isDragging) this.handleDrag(e.touches[0].clientX);
}, { passive: false });
this.canvas.addEventListener('touchend', () => this.isDragging = false);
}
Important: passive: false and e.preventDefault() on touchmove — otherwise browser scrolls page instead of rotating product.
Ready Libraries
For one-off tasks without special requirements:
- 360-image-viewer (npm) — light, vanilla JS, touch support
- Pannellum — for panoramas (equirectangular), not product shooting
- Three.js with equirectangular texture — for true spherical panorama
For React: react-360-image-viewer — wraps basic functionality, customization limited.
Auto-Rotation
On first viewport — auto rotate one revolution, stop. Demonstrates capability and hints interactivity.
autoSpin(rotations = 1, fps = 30) {
const totalFrames = this.frameUrls.length * rotations;
let frame = 0;
const interval = setInterval(() => {
this.currentFrame = (this.currentFrame + 1) % this.frameUrls.length;
this.render(this.currentFrame);
if (++frame >= totalFrames) clearInterval(interval);
}, 1000 / fps);
}
User Hints
Interactivity must be obvious. Standard solutions:
- "360°" icon over image in gallery
- "Drag to rotate" overlay — disappears on first interaction
- Left-right arrows (alternative control)
- Pause/Play buttons for auto-rotation
Gallery Integration
360° — one gallery slot. Thumbnail — special icon, not photo. On select launches 360 widget. On other slot — widget unmounts, frees memory (cancel pending loads via AbortController).
Timeline
- Ready library integration (react-360-image-viewer or similar): 3–5 days
- Custom implementation (Canvas API, progressive loading, touch): 1.5–2.5 weeks
- Shooting pipeline setup (if none): separate task outside development
- Batch frame conversion (WebP, optimization): 2–3 days
Shooting and content preparation often exceeds development cost. Account for this: 36 frames × 500 products — operational task, not technical.







