| | |
| | const YOUTUBE_VIDEO_ID = "_r0cHySFbeY"; |
| |
|
| | const stepsData = [ |
| | { timestamp: "00:00:00", title: "Apply the foot pads." }, |
| | { timestamp: "00:01:14", title: "Install the USB extension cable." }, |
| | { timestamp: "00:02:55", title: "Mount the body PCB." }, |
| | { timestamp: "00:04:30", title: "Connect the cables to the body PCB." }, |
| | { timestamp: "00:05:05", title: "Insert the ball bearing." }, |
| | { timestamp: "00:05:44", title: "Fit the body down assembly to the foot assembly." }, |
| | { timestamp: "00:06:14", title: "Mount the base foot motor to the body turning unit." }, |
| | { timestamp: "00:07:14", title: "Position the body turning assembly on the body down assembly." }, |
| | { timestamp: "00:08:25", title: "Secure the body turning assembly to the body down assembly." }, |
| | { timestamp: "00:10:15", title: "Screw the Stewart main plate in place." }, |
| | { timestamp: "00:13:10", title: "Connect the base foot motor." }, |
| | { timestamp: "00:14:13", title: "Screw the link rods onto the motor arms." }, |
| | { timestamp: "00:18:05", title: "Connect Motors 1 and 2." }, |
| | { timestamp: "00:18:44", title: "Connect Motors 2 and 3." }, |
| | { timestamp: "00:19:31", title: "Connect Motors 4 and 5." }, |
| | { timestamp: "00:20:19", title: "Connect Motors 5 and 6." }, |
| | { timestamp: "00:20:53", title: "Insert all motors into the Stewart main plate." }, |
| | { timestamp: "00:22:55", title: "Clip the motor cables into the body down assembly." }, |
| | { timestamp: "00:24:46", title: "Screw the speaker into the tricap." }, |
| | { timestamp: "00:25:39", title: "Position the tricap." }, |
| | { timestamp: "00:27:01", title: "Connect Motors 3 and 4." }, |
| | { timestamp: "00:27:44", title: "Route the cables." }, |
| | { timestamp: "00:28:04", title: "Secure the tricap with screws." }, |
| | { timestamp: "00:30:47", title: "Screw the bottom head onto the link rods." }, |
| | { timestamp: "00:35:17", title: "Route the cables through the bottom head." }, |
| | { timestamp: "00:35:49", title: "Route the cables through the head PCB." }, |
| | { timestamp: "00:36:10", title: "Screw the head PCB in place." }, |
| | { timestamp: "00:37:38", title: "Position the top shell." }, |
| | { timestamp: "00:38:42", title: "Screw the top shell in place." }, |
| | { timestamp: "00:41:08", title: "Place the lenses in the glasses holder." }, |
| | { timestamp: "00:41:38", title: "Insert the fisheye lenses into the caps." }, |
| | { timestamp: "00:42:14", title: "Snap the fisheye lenses into the glasses holder." }, |
| | { timestamp: "00:43:07", title: "Position the Arducam camera." }, |
| | { timestamp: "00:43:35", title: "Screw the Arducam camera in place." }, |
| | { timestamp: "00:44:08", title: "Screw the glasses assembly onto the front head shell." }, |
| | { timestamp: "00:45:51", title: "Plug in USB-C." }, |
| | { timestamp: "00:46:06", title: "Attach the cases to the antenna motors." }, |
| | { timestamp: "00:46:46", title: "Mount the motor assembly to the back head shell." }, |
| | { timestamp: "00:50:11", title: "Connect the antenna motors." }, |
| | { timestamp: "00:51:13", title: "Slide the back-head assembly onto the Reachy Mini body." }, |
| | { timestamp: "00:51:45", title: "Screw the back head in place." }, |
| | { timestamp: "00:53:35", title: "Attach the cable holder." }, |
| | { timestamp: "00:54:29", title: "Connect the speaker and motor cables." }, |
| | { timestamp: "00:55:22", title: "Connect the power and USB extension cables." }, |
| | { timestamp: "00:55:48", title: "Connect the flexible printed cable to the top head PCB." }, |
| | { timestamp: "00:56:23", title: "Slide the top-head assembly onto the back head." }, |
| | { timestamp: "00:56:31", title: "Connect the flexible printed cable to the head PCB." }, |
| | { timestamp: "00:57:18", title: "Plug the USB-C cable into the head PCB." }, |
| | { timestamp: "00:58:09", title: "Attach the front head." }, |
| | { timestamp: "00:59:20", title: "Assemble the antennas." }, |
| | { timestamp: "01:00:00", title: "Attach the antennas to the head." }, |
| | ]; |
| |
|
| | |
| | function parseTimestamp(ts) { |
| | const parts = ts.split(':').map(Number); |
| | return parts[0] * 3600 + parts[1] * 60 + parts[2]; |
| | } |
| |
|
| | |
| | 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/step3bis.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', |
| | 48: 'assets/step48.jpg', |
| | 49: 'assets/step49.jpg', |
| | 50: 'assets/step50.jpg', |
| | 51: 'assets/step51.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; |
| | |
| | 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 ${isActive ? 'step-indicator-active' : 'step-indicator-inactive'}`; |
| | button.setAttribute('aria-label', `Go to step ${stepNum}`); |
| | button.addEventListener('click', () => onClick(stepNum)); |
| | container.appendChild(button); |
| | } |
| | } |
| |
|
| | |
| | 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(); |
| |
|