|
|
|
|
|
const YOUTUBE_VIDEO_ID = "PC5Yx950nMY"; |
|
|
|
|
|
const stepsData = [ |
|
|
{ timestamp: "00:14", title: "Set all parts aside" }, |
|
|
{ timestamp: "00:30", title: "Stick Foot Pads" }, |
|
|
{ timestamp: "01:18", title: "Fix the USB Extension Cable and add the protective sleeve" }, |
|
|
{ timestamp: "02:58", title: "Insert the Power Board" }, |
|
|
{ timestamp: "03:17", title: "Fix the Power Board" }, |
|
|
{ timestamp: "04:06", title: "Connect the cables to the Power Board" }, |
|
|
{ timestamp: "05:26", title: "Position the Bottom Assembly" }, |
|
|
{ timestamp: "07:20", title: "Fix the Foot Assembly to the Bottom Assembly" }, |
|
|
{ timestamp: "08:15", title: "Connect the Foot Motor" }, |
|
|
{ timestamp: "09:15", title: "Screw the link rods onto the motor arms" }, |
|
|
{ timestamp: "12:22", title: "Connect Motor 1 to 2" }, |
|
|
{ timestamp: "13:11", title: "Connect Motor 2 to 3" }, |
|
|
{ timestamp: "13:36", title: "Connect Motor 4 to 5" }, |
|
|
{ timestamp: "13:55", title: "Connect Motor 5 to 6" }, |
|
|
{ timestamp: "14:24", title: "Insert all motors into the Bottom Assembly" }, |
|
|
{ timestamp: "15:54", title: "Clip the motor's cables into the Bottom Assembly" }, |
|
|
{ timestamp: "19:00", title: "Position the Tricap" }, |
|
|
{ timestamp: "19:19", title: "Route the cables" }, |
|
|
{ timestamp: "19:51", title: "Connect Motor 3 to 4" }, |
|
|
{ timestamp: "20:40", title: "Route the cables" }, |
|
|
{ timestamp: "22:02", title: "Check rotation" }, |
|
|
{ timestamp: "22:24", title: "Screw the Tricap" }, |
|
|
{ timestamp: "23:56", title: "Screw the Bottom Head onto the link rods" }, |
|
|
{ timestamp: "27:50", title: "Route the cables through the Bottom Head" }, |
|
|
{ timestamp: "28:45", title: "Connect the flexible camera cable (step for reference only)" }, |
|
|
{ timestamp: "28:51", title: "Route the cables through the Head PCB" }, |
|
|
{ timestamp: "29:16", title: "Screw the Head PCB" }, |
|
|
{ timestamp: "30:19", title: "Position the Top Shell" }, |
|
|
{ timestamp: "30:53", title: "Screw the Top Shell" }, |
|
|
{ timestamp: "32:18", title: "Place the lenses in the Glasses Holder" }, |
|
|
{ timestamp: "32:30", title: "Insert the Fisheye Lenses into the caps" }, |
|
|
{ timestamp: "32:54", title: "Snap the Fisheye Lenses" }, |
|
|
{ timestamp: "33:39", title: "Screw the Glasses Assembly onto the Front Head Shell" }, |
|
|
{ timestamp: "34:40", title: "Attach the cases to Antenna's motors" }, |
|
|
{ timestamp: "34:59", title: "Fix Antenna's Assembly to Back Head Shell" }, |
|
|
{ timestamp: "36:30", title: "Connect the Antenna's motors" }, |
|
|
{ timestamp: "36:52", title: "Slide the Back Head Assembly onto the Reachy Mini body" }, |
|
|
{ timestamp: "37:14", title: "Screw the Back Head" }, |
|
|
{ timestamp: "38:00", title: "Fix the Cable Holder" }, |
|
|
{ timestamp: "39:45", title: "Connect Speaker and Motor Cables" }, |
|
|
{ timestamp: "40:07", title: "Connect Power and USB Extension Cables" }, |
|
|
{ timestamp: "40:25", title: "Slide the Top Head Assembly onto the Back Head" }, |
|
|
{ timestamp: "40:39", title: "Connect the Flexible Printed Cable to the Head PCB" }, |
|
|
{ timestamp: "40:50", title: "Connect the Flexible Camera Cable on the Front Head" }, |
|
|
{ timestamp: "41:42", title: "Fix the Front Head" }, |
|
|
{ timestamp: "42:56", title: "Fix the Antennas" }, |
|
|
]; |
|
|
|
|
|
|
|
|
function parseTimestamp(ts) { |
|
|
const parts = ts.split(':').map(Number); |
|
|
return parts[0] * 60 + parts[1]; |
|
|
} |
|
|
|
|
|
|
|
|
const assemblySteps = stepsData.map((step, index) => ({ |
|
|
id: index + 1, |
|
|
title: step.title, |
|
|
timestamp: step.timestamp, |
|
|
timestampSeconds: parseTimestamp(step.timestamp) |
|
|
})); |
|
|
|
|
|
const TOTAL_STEPS = assemblySteps.length; |
|
|
|
|
|
|
|
|
const availableImages = { |
|
|
1: "assets/step1.jpg", |
|
|
2: "assets/step2.jpg", |
|
|
3: "assets/step3.jpg", |
|
|
4: "assets/step4.jpg", |
|
|
5: "assets/step5.jpg", |
|
|
6: "assets/step6.jpg", |
|
|
7: "assets/step7.jpg", |
|
|
8: "assets/step8.jpg", |
|
|
9: "assets/step9.jpg", |
|
|
10: "assets/step10.jpg", |
|
|
11: "assets/step11.jpg", |
|
|
12: "assets/step12.jpg", |
|
|
13: "assets/step13.jpg", |
|
|
14: "assets/step14.jpg", |
|
|
15: "assets/step15.jpg", |
|
|
16: "assets/step16.jpg", |
|
|
17: "assets/step17.jpg", |
|
|
18: "assets/step18.jpg", |
|
|
19: "assets/step19.jpg", |
|
|
20: "assets/step20.jpg", |
|
|
21: "assets/step21.jpg", |
|
|
22: "assets/step22.jpg", |
|
|
23: "assets/step23.jpg", |
|
|
24: "assets/step24.jpg", |
|
|
25: "assets/step25.jpg", |
|
|
26: "assets/step26.jpg", |
|
|
27: "assets/step27.jpg", |
|
|
28: "assets/step28.jpg", |
|
|
29: "assets/step29.jpg", |
|
|
30: "assets/step30.jpg", |
|
|
31: "assets/step31.jpg", |
|
|
32: "assets/step32.jpg", |
|
|
33: "assets/step33.jpg", |
|
|
34: "assets/step34.jpg", |
|
|
35: "assets/step35.jpg", |
|
|
36: "assets/step36.jpg", |
|
|
37: "assets/step37.jpg", |
|
|
38: "assets/step38.jpg", |
|
|
39: "assets/step39.jpg", |
|
|
40: "assets/step40.jpg", |
|
|
41: "assets/step41.jpg", |
|
|
42: "assets/step42.jpg", |
|
|
43: "assets/step43.jpg", |
|
|
44: "assets/step44.jpg", |
|
|
45: "assets/step45.jpg", |
|
|
46: "assets/step46.jpg", |
|
|
47: "assets/step47.jpg", |
|
|
}; |
|
|
|
|
|
function getStepImage(stepId) { |
|
|
return availableImages[stepId] || null; |
|
|
} |
|
|
|
|
|
|
|
|
let currentStep = 1; |
|
|
let isFullscreen = false; |
|
|
let scale = 1; |
|
|
let position = { x: 0, y: 0 }; |
|
|
let isDragging = false; |
|
|
let dragStart = { x: 0, y: 0 }; |
|
|
|
|
|
|
|
|
const stepCounterText = document.getElementById('step-counter-text'); |
|
|
const stepImage = document.getElementById('step-image'); |
|
|
const placeholder = document.getElementById('placeholder'); |
|
|
const placeholderNumber = document.getElementById('placeholder-number'); |
|
|
const imageWrapper = document.getElementById('image-wrapper'); |
|
|
const youtubeIframeDesktop = document.getElementById('youtube-iframe-desktop'); |
|
|
const youtubeIframeMobile = document.getElementById('youtube-iframe-mobile'); |
|
|
const prevBtn = document.getElementById('prev-btn'); |
|
|
const nextBtn = document.getElementById('next-btn'); |
|
|
const stepIndicators = document.getElementById('step-indicators'); |
|
|
const progressBar = document.getElementById('progress-bar'); |
|
|
const fullscreenBtn = document.getElementById('fullscreen-btn'); |
|
|
const fullscreenModal = document.getElementById('fullscreen-modal'); |
|
|
const closeFullscreenBtn = document.getElementById('close-fullscreen-btn'); |
|
|
const fullscreenStepTitle = document.getElementById('fullscreen-step-title'); |
|
|
const fullscreenImage = document.getElementById('fullscreen-image'); |
|
|
const fullscreenPlaceholder = document.getElementById('fullscreen-placeholder'); |
|
|
const fullscreenPlaceholderNumber = document.getElementById('fullscreen-placeholder-number'); |
|
|
const fullscreenImageContainer = document.getElementById('fullscreen-image-container'); |
|
|
const fullscreenYoutubeIframeDesktop = document.getElementById('fullscreen-youtube-iframe-desktop'); |
|
|
const fullscreenYoutubeIframeMobile = document.getElementById('fullscreen-youtube-iframe-mobile'); |
|
|
const fullscreenPrevBtn = document.getElementById('fullscreen-prev-btn'); |
|
|
const fullscreenNextBtn = document.getElementById('fullscreen-next-btn'); |
|
|
const fullscreenStepIndicators = document.getElementById('fullscreen-step-indicators'); |
|
|
const fullscreenProgressBar = document.getElementById('fullscreen-progress-bar'); |
|
|
const zoomInBtn = document.getElementById('zoom-in-btn'); |
|
|
const zoomOutBtn = document.getElementById('zoom-out-btn'); |
|
|
const zoomLevel = document.getElementById('zoom-level'); |
|
|
|
|
|
|
|
|
function updateYouTubeEmbed(timestampSeconds) { |
|
|
const embedUrl = `https://www.youtube.com/embed/${YOUTUBE_VIDEO_ID}?start=${timestampSeconds}&rel=0&autoplay=1&mute=1`; |
|
|
youtubeIframeDesktop.src = embedUrl; |
|
|
youtubeIframeMobile.src = embedUrl; |
|
|
fullscreenYoutubeIframeDesktop.src = embedUrl; |
|
|
fullscreenYoutubeIframeMobile.src = embedUrl; |
|
|
} |
|
|
|
|
|
|
|
|
function renderStepIndicators(containerId, currentStep, onClick) { |
|
|
const container = document.getElementById(containerId); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
const groupStart = Math.floor((currentStep - 1) / 10) * 10 + 1; |
|
|
const groupEnd = Math.min(groupStart + 9, TOTAL_STEPS); |
|
|
const canGoPrevGroup = groupStart > 1; |
|
|
const canGoNextGroup = groupEnd < TOTAL_STEPS; |
|
|
|
|
|
|
|
|
if (canGoPrevGroup) { |
|
|
const prevGroupBtn = document.createElement('button'); |
|
|
prevGroupBtn.className = 'step-indicator-nav'; |
|
|
prevGroupBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>'; |
|
|
prevGroupBtn.setAttribute('aria-label', 'Previous group'); |
|
|
prevGroupBtn.addEventListener('click', () => onClick(groupStart - 1)); |
|
|
container.appendChild(prevGroupBtn); |
|
|
} |
|
|
|
|
|
|
|
|
for (let i = 0; i < 10; i++) { |
|
|
const stepNum = groupStart + i; |
|
|
if (stepNum > TOTAL_STEPS) break; |
|
|
|
|
|
const isActive = stepNum === currentStep; |
|
|
const button = document.createElement('button'); |
|
|
button.className = `step-indicator-num ${isActive ? 'step-indicator-num-active' : ''}`; |
|
|
button.textContent = stepNum; |
|
|
button.setAttribute('aria-label', `Go to step ${stepNum}`); |
|
|
button.addEventListener('click', () => onClick(stepNum)); |
|
|
container.appendChild(button); |
|
|
} |
|
|
|
|
|
|
|
|
if (canGoNextGroup) { |
|
|
const nextGroupBtn = document.createElement('button'); |
|
|
nextGroupBtn.className = 'step-indicator-nav'; |
|
|
nextGroupBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>'; |
|
|
nextGroupBtn.setAttribute('aria-label', 'Next group'); |
|
|
nextGroupBtn.addEventListener('click', () => onClick(groupEnd + 1)); |
|
|
container.appendChild(nextGroupBtn); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateUI() { |
|
|
const step = assemblySteps[currentStep - 1]; |
|
|
const imageSrc = getStepImage(step.id); |
|
|
|
|
|
|
|
|
stepCounterText.textContent = `Step ${step.id}/${TOTAL_STEPS}`; |
|
|
|
|
|
|
|
|
if (imageSrc) { |
|
|
stepImage.src = imageSrc; |
|
|
stepImage.alt = `Assembly step ${step.id}`; |
|
|
stepImage.classList.remove('hidden'); |
|
|
placeholder.classList.add('hidden'); |
|
|
} else { |
|
|
stepImage.classList.add('hidden'); |
|
|
placeholder.classList.remove('hidden'); |
|
|
placeholderNumber.textContent = step.id; |
|
|
} |
|
|
|
|
|
|
|
|
updateYouTubeEmbed(step.timestampSeconds); |
|
|
|
|
|
|
|
|
prevBtn.disabled = currentStep === 1; |
|
|
nextBtn.disabled = currentStep === TOTAL_STEPS; |
|
|
|
|
|
|
|
|
renderStepIndicators('step-indicators', currentStep, goToStep); |
|
|
|
|
|
|
|
|
const progress = (currentStep / TOTAL_STEPS) * 100; |
|
|
progressBar.style.width = `${progress}%`; |
|
|
|
|
|
|
|
|
updateFullscreenUI(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateFullscreenUI() { |
|
|
const step = assemblySteps[currentStep - 1]; |
|
|
const imageSrc = getStepImage(step.id); |
|
|
|
|
|
fullscreenStepTitle.textContent = `Step ${step.id}/${TOTAL_STEPS} - ${step.title}`; |
|
|
|
|
|
if (imageSrc) { |
|
|
fullscreenImage.src = imageSrc; |
|
|
fullscreenImage.alt = `Step ${step.id}`; |
|
|
fullscreenImage.classList.remove('hidden'); |
|
|
fullscreenPlaceholder.classList.add('hidden'); |
|
|
} else { |
|
|
fullscreenImage.classList.add('hidden'); |
|
|
fullscreenPlaceholder.classList.remove('hidden'); |
|
|
fullscreenPlaceholderNumber.textContent = step.id; |
|
|
} |
|
|
|
|
|
fullscreenPrevBtn.disabled = currentStep === 1; |
|
|
fullscreenNextBtn.disabled = currentStep === TOTAL_STEPS; |
|
|
|
|
|
renderStepIndicators('fullscreen-step-indicators', currentStep, goToStepFullscreen); |
|
|
|
|
|
const progress = (currentStep / TOTAL_STEPS) * 100; |
|
|
fullscreenProgressBar.style.width = `${progress}%`; |
|
|
|
|
|
updateZoomDisplay(); |
|
|
} |
|
|
|
|
|
|
|
|
function goToPrevious() { |
|
|
if (currentStep > 1) { |
|
|
currentStep--; |
|
|
updateUI(); |
|
|
} |
|
|
} |
|
|
|
|
|
function goToNext() { |
|
|
if (currentStep < TOTAL_STEPS) { |
|
|
currentStep++; |
|
|
updateUI(); |
|
|
} |
|
|
} |
|
|
|
|
|
function goToStep(step) { |
|
|
if (step >= 1 && step <= TOTAL_STEPS) { |
|
|
currentStep = step; |
|
|
updateUI(); |
|
|
} |
|
|
} |
|
|
|
|
|
function goToStepFullscreen(step) { |
|
|
resetZoom(); |
|
|
goToStep(step); |
|
|
} |
|
|
|
|
|
function goToPreviousFullscreen() { |
|
|
resetZoom(); |
|
|
goToPrevious(); |
|
|
} |
|
|
|
|
|
function goToNextFullscreen() { |
|
|
resetZoom(); |
|
|
goToNext(); |
|
|
} |
|
|
|
|
|
|
|
|
function openFullscreen() { |
|
|
isFullscreen = true; |
|
|
fullscreenModal.classList.remove('hidden'); |
|
|
document.body.style.overflow = 'hidden'; |
|
|
updateFullscreenUI(); |
|
|
} |
|
|
|
|
|
function closeFullscreen() { |
|
|
isFullscreen = false; |
|
|
fullscreenModal.classList.add('hidden'); |
|
|
document.body.style.overflow = ''; |
|
|
resetZoom(); |
|
|
} |
|
|
|
|
|
|
|
|
function zoomIn() { |
|
|
scale = Math.min(scale + 0.5, 4); |
|
|
updateZoomDisplay(); |
|
|
} |
|
|
|
|
|
function zoomOut() { |
|
|
scale = Math.max(scale - 0.5, 0.5); |
|
|
updateZoomDisplay(); |
|
|
} |
|
|
|
|
|
function resetZoom() { |
|
|
scale = 1; |
|
|
position = { x: 0, y: 0 }; |
|
|
updateZoomDisplay(); |
|
|
} |
|
|
|
|
|
function updateZoomDisplay() { |
|
|
zoomLevel.textContent = `${Math.round(scale * 100)}%`; |
|
|
fullscreenImage.style.transform = `translate(${position.x}px, ${position.y}px) scale(${scale})`; |
|
|
} |
|
|
|
|
|
|
|
|
function handleMouseDown(e) { |
|
|
if (scale > 1) { |
|
|
isDragging = true; |
|
|
dragStart = { |
|
|
x: e.clientX - position.x, |
|
|
y: e.clientY - position.y |
|
|
}; |
|
|
fullscreenImageContainer.style.cursor = 'grabbing'; |
|
|
} |
|
|
} |
|
|
|
|
|
function handleMouseMove(e) { |
|
|
if (isDragging && scale > 1) { |
|
|
position = { |
|
|
x: e.clientX - dragStart.x, |
|
|
y: e.clientY - dragStart.y |
|
|
}; |
|
|
updateZoomDisplay(); |
|
|
} |
|
|
} |
|
|
|
|
|
function handleMouseUp() { |
|
|
isDragging = false; |
|
|
fullscreenImageContainer.style.cursor = 'grab'; |
|
|
} |
|
|
|
|
|
function handleWheel(e) { |
|
|
e.preventDefault(); |
|
|
const delta = e.deltaY > 0 ? -0.2 : 0.2; |
|
|
scale = Math.max(0.5, Math.min(4, scale + delta)); |
|
|
updateZoomDisplay(); |
|
|
} |
|
|
|
|
|
|
|
|
prevBtn.addEventListener('click', goToPrevious); |
|
|
nextBtn.addEventListener('click', goToNext); |
|
|
fullscreenBtn.addEventListener('click', openFullscreen); |
|
|
stepImage.addEventListener('click', openFullscreen); |
|
|
closeFullscreenBtn.addEventListener('click', closeFullscreen); |
|
|
fullscreenPrevBtn.addEventListener('click', goToPreviousFullscreen); |
|
|
fullscreenNextBtn.addEventListener('click', goToNextFullscreen); |
|
|
zoomInBtn.addEventListener('click', zoomIn); |
|
|
zoomOutBtn.addEventListener('click', zoomOut); |
|
|
|
|
|
fullscreenImageContainer.addEventListener('mousedown', handleMouseDown); |
|
|
fullscreenImageContainer.addEventListener('mousemove', handleMouseMove); |
|
|
fullscreenImageContainer.addEventListener('mouseup', handleMouseUp); |
|
|
fullscreenImageContainer.addEventListener('mouseleave', handleMouseUp); |
|
|
fullscreenImageContainer.addEventListener('wheel', handleWheel, { passive: false }); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
if (isFullscreen) { |
|
|
if (e.key === 'Escape') closeFullscreen(); |
|
|
if (e.key === '+' || e.key === '=') zoomIn(); |
|
|
if (e.key === '-') zoomOut(); |
|
|
if (e.key === '0') resetZoom(); |
|
|
if (e.key === 'ArrowLeft' && currentStep > 1) goToPreviousFullscreen(); |
|
|
if (e.key === 'ArrowRight' && currentStep < TOTAL_STEPS) goToNextFullscreen(); |
|
|
} else { |
|
|
if (e.key === 'ArrowRight') goToNext(); |
|
|
if (e.key === 'ArrowLeft') goToPrevious(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
updateUI(); |
|
|
|